@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 +268 -50
- package/package.json +5 -2
- package/studio-plugin/MCPInspectorPlugin.rbxmx +83 -20
- package/studio-plugin/MCPPlugin.rbxmx +83 -20
- package/studio-plugin/package-lock.json +9 -9
- package/studio-plugin/src/modules/Communication.ts +26 -6
- package/studio-plugin/src/modules/LuauExec.ts +6 -3
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(
|
|
8701
|
-
const typeBytes = Buffer.from(
|
|
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
|
|
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
|
-
|
|
15147
|
-
|
|
15148
|
-
if (process.platform === "darwin") {
|
|
15149
|
-
|
|
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
|
-
|
|
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
|
|
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() &&
|
|
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:
|
|
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) || !
|
|
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 (
|
|
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 (!
|
|
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) =>
|
|
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() &&
|
|
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
|
|
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
|
|
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 (!
|
|
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 {
|
|
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 =
|
|
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}` } : {
|
|
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({
|
|
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
|
|
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` } : {
|
|
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
|
-
|
|
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 ||
|
|
19102
|
-
detail: `plugin v${
|
|
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 ||
|
|
19107
|
-
detail: `plugin protocol ${
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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) =>
|
|
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 (!
|
|
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
|
|
19309
|
-
const asset =
|
|
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 ${
|
|
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 ${
|
|
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("--
|
|
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(
|
|
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(
|
|
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": "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
|
1150
|
-
if
|
|
1184
|
+
local _condition_2 = mcpConnected
|
|
1185
|
+
if _condition_2 then
|
|
1151
1186
|
local _value = (string.find(el.statusLabel.Text, "Connected"))
|
|
1152
|
-
|
|
1187
|
+
_condition_2 = not (_value ~= 0 and _value == _value and _value)
|
|
1153
1188
|
end
|
|
1154
|
-
if
|
|
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
|
|
1186
|
-
if
|
|
1187
|
-
|
|
1220
|
+
local _condition_3 = conn.mcpWaitStartTime
|
|
1221
|
+
if _condition_3 == nil then
|
|
1222
|
+
_condition_3 = tick()
|
|
1188
1223
|
end
|
|
1189
|
-
local elapsed = _exp -
|
|
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
|
-
|
|
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
|
-
-- "
|
|
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)}-
|
|
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 (
|
|
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 (
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
|
1150
|
-
if
|
|
1184
|
+
local _condition_2 = mcpConnected
|
|
1185
|
+
if _condition_2 then
|
|
1151
1186
|
local _value = (string.find(el.statusLabel.Text, "Connected"))
|
|
1152
|
-
|
|
1187
|
+
_condition_2 = not (_value ~= 0 and _value == _value and _value)
|
|
1153
1188
|
end
|
|
1154
|
-
if
|
|
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
|
|
1186
|
-
if
|
|
1187
|
-
|
|
1220
|
+
local _condition_3 = conn.mcpWaitStartTime
|
|
1221
|
+
if _condition_3 == nil then
|
|
1222
|
+
_condition_3 = tick()
|
|
1188
1223
|
end
|
|
1189
|
-
local elapsed = _exp -
|
|
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
|
-
|
|
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
|
-
-- "
|
|
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)}-
|
|
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 (
|
|
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 (
|
|
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 = "
|
|
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.
|
|
71
|
-
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.
|
|
72
|
-
"integrity": "sha512-
|
|
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.
|
|
238
|
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.
|
|
239
|
-
"integrity": "sha512-
|
|
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.
|
|
470
|
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.
|
|
471
|
-
"integrity": "sha512-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
419
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|