@chrrxs/robloxstudio-mcp-inspector 2.15.0 → 2.15.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
@@ -19,6 +19,10 @@ function toPublic(inst) {
19
19
  placeName: inst.placeName,
20
20
  dataModelName: inst.dataModelName,
21
21
  isRunning: inst.isRunning,
22
+ pluginVersion: inst.pluginVersion,
23
+ pluginVariant: inst.pluginVariant,
24
+ serverVersion: inst.serverVersion,
25
+ versionMismatch: inst.versionMismatch,
22
26
  lastActivity: inst.lastActivity,
23
27
  connectedAt: inst.connectedAt
24
28
  };
@@ -45,6 +49,10 @@ var init_bridge_service = __esm({
45
49
  const { pluginSessionId, instanceId, role } = input;
46
50
  const prior = this.instances.get(pluginSessionId);
47
51
  let assignedRole = role;
52
+ const pluginVersion = input.pluginVersion ?? "";
53
+ const pluginVariant = input.pluginVariant ?? "unknown";
54
+ const serverVersion = input.serverVersion ?? "";
55
+ const versionMismatch = pluginVersion !== "" && serverVersion !== "" && pluginVersion !== serverVersion;
48
56
  if (role === "client") {
49
57
  if (prior && prior.instanceId === instanceId && prior.role.match(/^client-\d+$/)) {
50
58
  assignedRole = prior.role;
@@ -82,6 +90,10 @@ var init_bridge_service = __esm({
82
90
  placeName: input.placeName ?? "",
83
91
  dataModelName: input.dataModelName ?? "",
84
92
  isRunning: input.isRunning ?? false,
93
+ pluginVersion,
94
+ pluginVariant,
95
+ serverVersion,
96
+ versionMismatch,
85
97
  lastActivity: Date.now(),
86
98
  connectedAt: prior?.connectedAt ?? Date.now()
87
99
  });
@@ -326,6 +338,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
326
338
  let lastMCPActivity = 0;
327
339
  let mcpServerStartTime = 0;
328
340
  const proxyInstances = /* @__PURE__ */ new Set();
341
+ const warnedVersionMismatches = /* @__PURE__ */ new Set();
329
342
  const setMCPServerActive = (active) => {
330
343
  mcpServerActive = active;
331
344
  if (active) {
@@ -354,13 +367,16 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
354
367
  app.use(express.urlencoded({ limit: "50mb", extended: true }));
355
368
  app.get("/health", (req, res) => {
356
369
  const instances = bridge.getInstances();
370
+ const publicInstances = instances.map(toPublic);
357
371
  res.json({
358
372
  status: "ok",
359
373
  service: "robloxstudio-mcp",
360
374
  version: serverConfig?.version,
375
+ serverVersion: serverConfig?.version,
361
376
  pluginConnected: instances.length > 0,
362
377
  instanceCount: instances.length,
363
- instances: instances.map(toPublic),
378
+ instances: publicInstances,
379
+ versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
364
380
  mcpServerActive: isMCPServerActive(),
365
381
  uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0,
366
382
  pendingRequests: bridge.getPendingRequestCount(),
@@ -369,7 +385,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
369
385
  });
370
386
  });
371
387
  app.post("/ready", (req, res) => {
372
- const { pluginSessionId, instanceId, role, placeId, placeName, dataModelName, isRunning } = req.body;
388
+ const { pluginSessionId, instanceId, role, placeId, placeName, dataModelName, isRunning, pluginVersion, pluginVariant } = req.body;
373
389
  if (!pluginSessionId || !instanceId || !role) {
374
390
  res.status(400).json({
375
391
  success: false,
@@ -384,7 +400,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
384
400
  placeId: typeof placeId === "number" ? placeId : 0,
385
401
  placeName: typeof placeName === "string" ? placeName : "",
386
402
  dataModelName: typeof dataModelName === "string" ? dataModelName : "",
387
- isRunning: !!isRunning
403
+ isRunning: !!isRunning,
404
+ pluginVersion: typeof pluginVersion === "string" ? pluginVersion : "",
405
+ pluginVariant: typeof pluginVariant === "string" ? pluginVariant : "unknown",
406
+ serverVersion: serverConfig?.version ?? ""
388
407
  });
389
408
  if (!result.ok) {
390
409
  res.status(409).json({
@@ -395,10 +414,17 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
395
414
  });
396
415
  return;
397
416
  }
417
+ const registered = bridge.getInstanceBySessionId(pluginSessionId);
418
+ if (registered?.versionMismatch && !warnedVersionMismatches.has(pluginSessionId)) {
419
+ warnedVersionMismatches.add(pluginSessionId);
420
+ console.error(`[version-mismatch] Studio plugin v${registered.pluginVersion} (${registered.pluginVariant}) does not match MCP server v${registered.serverVersion} for ${registered.instanceId}/${registered.role}`);
421
+ }
398
422
  res.json({
399
423
  success: true,
400
424
  assignedRole: result.assignedRole,
401
- instanceId: result.instanceId
425
+ instanceId: result.instanceId,
426
+ serverVersion: serverConfig?.version,
427
+ versionMismatch: registered?.versionMismatch ?? false
402
428
  });
403
429
  });
404
430
  app.post("/disconnect", (req, res) => {
@@ -410,17 +436,25 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
410
436
  });
411
437
  app.get("/status", (req, res) => {
412
438
  const instances = bridge.getInstances();
439
+ const publicInstances = instances.map(toPublic);
413
440
  res.json({
414
441
  pluginConnected: instances.length > 0,
415
442
  instanceCount: instances.length,
416
- instances: instances.map(toPublic),
443
+ instances: publicInstances,
444
+ serverVersion: serverConfig?.version,
445
+ versionMismatch: publicInstances.some((inst) => inst.versionMismatch),
417
446
  mcpServerActive: isMCPServerActive(),
418
447
  lastMCPActivity,
419
448
  uptime: mcpServerActive ? Date.now() - mcpServerStartTime : 0
420
449
  });
421
450
  });
422
451
  app.get("/instances", (req, res) => {
423
- res.json({ instances: bridge.getInstances() });
452
+ const instances = bridge.getInstances();
453
+ res.json({
454
+ instances,
455
+ serverVersion: serverConfig?.version,
456
+ versionMismatch: instances.some((inst) => inst.versionMismatch)
457
+ });
424
458
  });
425
459
  app.get("/poll", (req, res) => {
426
460
  const pluginSessionId = req.query.pluginSessionId;
@@ -430,11 +464,17 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
430
464
  let callerInstanceId;
431
465
  let callerRole;
432
466
  let knownInstance = false;
467
+ let callerPluginVersion;
468
+ let callerPluginVariant;
469
+ let versionMismatch = false;
433
470
  if (pluginSessionId) {
434
471
  const inst = bridge.getInstanceBySessionId(pluginSessionId);
435
472
  if (inst) {
436
473
  callerInstanceId = inst.instanceId;
437
474
  callerRole = inst.role;
475
+ callerPluginVersion = inst.pluginVersion;
476
+ callerPluginVariant = inst.pluginVariant;
477
+ versionMismatch = inst.versionMismatch;
438
478
  knownInstance = true;
439
479
  }
440
480
  }
@@ -444,6 +484,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
444
484
  pluginConnected: true,
445
485
  mcpConnected: false,
446
486
  knownInstance,
487
+ serverVersion: serverConfig?.version,
488
+ pluginVersion: callerPluginVersion,
489
+ pluginVariant: callerPluginVariant,
490
+ versionMismatch,
447
491
  request: null
448
492
  });
449
493
  return;
@@ -456,6 +500,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
456
500
  mcpConnected: true,
457
501
  pluginConnected: true,
458
502
  knownInstance,
503
+ serverVersion: serverConfig?.version,
504
+ pluginVersion: callerPluginVersion,
505
+ pluginVariant: callerPluginVariant,
506
+ versionMismatch,
459
507
  proxyInstanceCount: proxyInstances.size
460
508
  });
461
509
  } else {
@@ -464,6 +512,10 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
464
512
  mcpConnected: true,
465
513
  pluginConnected: true,
466
514
  knownInstance,
515
+ serverVersion: serverConfig?.version,
516
+ pluginVersion: callerPluginVersion,
517
+ pluginVariant: callerPluginVariant,
518
+ versionMismatch,
467
519
  proxyInstanceCount: proxyInstances.size
468
520
  });
469
521
  }
@@ -7283,20 +7335,20 @@ function getPluginsFolder() {
7283
7335
  }
7284
7336
  return join2(homedir2(), "Documents", "Roblox", "Plugins");
7285
7337
  }
7286
- function handleVariantConflict({ pluginsFolder, otherAssetName, replace }) {
7338
+ function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
7287
7339
  const otherDest = join2(pluginsFolder, otherAssetName);
7288
7340
  if (!existsSync2(otherDest))
7289
7341
  return;
7290
7342
  if (replace) {
7291
7343
  try {
7292
7344
  unlinkSync(otherDest);
7293
- console.log(`Removed conflicting ${otherAssetName}.`);
7345
+ log(`Removed conflicting ${otherAssetName}.`);
7294
7346
  } catch (err) {
7295
- console.warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
7347
+ warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
7296
7348
  }
7297
7349
  return;
7298
7350
  }
7299
- console.warn(`
7351
+ warn(`
7300
7352
  [install-plugin] WARNING: ${otherAssetName} is already present in ${pluginsFolder}.
7301
7353
  Only one MCP plugin variant should be present. If both variants are in the Studio Plugins folder, Studio loads both and runtime routing can become unpredictable.
7302
7354
  Re-run with --replace-variant to remove ${otherAssetName}, or delete it manually.
@@ -7327,10 +7379,12 @@ var init_dist = __esm({
7327
7379
  // src/install-plugin.ts
7328
7380
  var install_plugin_exports = {};
7329
7381
  __export(install_plugin_exports, {
7382
+ installBundledPlugin: () => installBundledPlugin,
7330
7383
  installPlugin: () => installPlugin
7331
7384
  });
7332
- import { createWriteStream, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
7333
- import { join as join3 } from "path";
7385
+ import { copyFileSync, createWriteStream, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync2 } from "fs";
7386
+ import { dirname as dirname2, join as join3 } from "path";
7387
+ import { fileURLToPath } from "url";
7334
7388
  import { get } from "https";
7335
7389
  function httpsGet(url) {
7336
7390
  return new Promise((resolve2, reject) => {
@@ -7383,8 +7437,11 @@ async function fetchJson(url) {
7383
7437
  }
7384
7438
  return JSON.parse(Buffer.concat(chunks).toString());
7385
7439
  }
7386
- async function installPlugin() {
7387
- const replaceVariant = process.argv.includes("--replace-variant");
7440
+ function prepareInstall({
7441
+ replaceVariant,
7442
+ log,
7443
+ warn
7444
+ }) {
7388
7445
  const pluginsFolder = getPluginsFolder();
7389
7446
  if (!existsSync3(pluginsFolder)) {
7390
7447
  mkdirSync2(pluginsFolder, { recursive: true });
@@ -7392,18 +7449,55 @@ async function installPlugin() {
7392
7449
  handleVariantConflict({
7393
7450
  pluginsFolder,
7394
7451
  otherAssetName: OTHER_VARIANT,
7395
- replace: replaceVariant
7452
+ replace: replaceVariant,
7453
+ log,
7454
+ warn
7396
7455
  });
7397
- console.log("Fetching latest release...");
7456
+ return pluginsFolder;
7457
+ }
7458
+ function bundledAssetPath() {
7459
+ const currentDir = dirname2(fileURLToPath(import.meta.url));
7460
+ const candidates = [
7461
+ join3(currentDir, "..", "studio-plugin", ASSET_NAME),
7462
+ join3(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
7463
+ ];
7464
+ return candidates.find((candidate) => existsSync3(candidate)) ?? null;
7465
+ }
7466
+ function filesMatch(a, b) {
7467
+ if (!existsSync3(b)) return false;
7468
+ const aBytes = readFileSync3(a);
7469
+ const bBytes = readFileSync3(b);
7470
+ return aBytes.length === bBytes.length && aBytes.equals(bBytes);
7471
+ }
7472
+ async function installBundledPlugin(options = {}) {
7473
+ const log = options.log ?? console.log;
7474
+ const warn = options.warn ?? console.warn;
7475
+ const replaceVariant = options.replaceVariant ?? process.argv.includes("--replace-variant");
7476
+ const source = bundledAssetPath();
7477
+ if (!source) {
7478
+ throw new Error(`Bundled ${ASSET_NAME} not found in package`);
7479
+ }
7480
+ const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
7481
+ const dest = join3(pluginsFolder, ASSET_NAME);
7482
+ if (filesMatch(source, dest)) return;
7483
+ copyFileSync(source, dest);
7484
+ log(`Installed ${ASSET_NAME} to ${dest}`);
7485
+ }
7486
+ async function installPlugin(options = {}) {
7487
+ const replaceVariant = options.replaceVariant ?? process.argv.includes("--replace-variant");
7488
+ const log = options.log ?? console.log;
7489
+ const warn = options.warn ?? console.warn;
7490
+ const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
7491
+ log("Fetching latest release...");
7398
7492
  const release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
7399
7493
  const asset = release.assets?.find((a) => a.name === ASSET_NAME);
7400
7494
  if (!asset) {
7401
7495
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
7402
7496
  }
7403
7497
  const dest = join3(pluginsFolder, ASSET_NAME);
7404
- console.log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
7498
+ log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
7405
7499
  await download(asset.browser_download_url, dest);
7406
- console.log(`Installed to ${dest}`);
7500
+ log(`Installed to ${dest}`);
7407
7501
  }
7408
7502
  var REPO, ASSET_NAME, OTHER_VARIANT, TIMEOUT_MS, MAX_REDIRECTS;
7409
7503
  var init_install_plugin = __esm({
@@ -7428,6 +7522,18 @@ if (process.argv.includes("--install-plugin")) {
7428
7522
  process.exitCode = 1;
7429
7523
  });
7430
7524
  } else {
7525
+ if (process.argv.includes("--auto-install-plugin")) {
7526
+ const { installBundledPlugin: installBundledPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
7527
+ await installBundledPlugin2({
7528
+ replaceVariant: process.argv.includes("--replace-variant"),
7529
+ log: (message) => console.error(`[install-plugin] ${message}`),
7530
+ warn: (message) => console.error(message)
7531
+ }).catch((err) => {
7532
+ console.error(
7533
+ `[install-plugin] Auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
7534
+ );
7535
+ });
7536
+ }
7431
7537
  const require2 = createRequire(import.meta.url);
7432
7538
  const { version: VERSION } = require2("../package.json");
7433
7539
  const server = new RobloxStudioMCPServer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp-inspector",
3
- "version": "2.15.0",
3
+ "version": "2.15.1",
4
4
  "description": "Read-only MCP server for inspecting and debugging Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "license": "MIT",
31
31
  "repository": {
32
32
  "type": "git",
33
- "url": "https://github.com/chrrxs/robloxstudio-mcp.git"
33
+ "url": "git+https://github.com/chrrxs/robloxstudio-mcp.git"
34
34
  },
35
35
  "homepage": "https://github.com/chrrxs/robloxstudio-mcp#readme",
36
36
  "bugs": {
@@ -15,6 +15,7 @@ Complete your AI assistant integration with this easy-to-install Studio plugin.
15
15
  ### Method 2: Direct Download
16
16
  1. **Download the plugin:**
17
17
  - **GitHub Release**: [Download MCPPlugin.rbxmx](https://github.com/chrrxs/robloxstudio-mcp/releases/latest/download/MCPPlugin.rbxmx)
18
+ - **CLI installer**: `npx -y @chrrxs/robloxstudio-mcp@latest --install-plugin`
18
19
  - This is the official Roblox plugin format
19
20
 
20
21
  2. **Install to plugins folder:**
@@ -54,7 +55,12 @@ Choose your AI assistant:
54
55
 
55
56
  **For Claude Code:**
56
57
  ```bash
57
- claude mcp add robloxstudio-mcp
58
+ claude mcp add robloxstudio -- npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin
59
+ ```
60
+
61
+ **For Codex CLI:**
62
+ ```bash
63
+ codex mcp add robloxstudio -- npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin
58
64
  ```
59
65
 
60
66
  **For Claude Desktop/Others:**
@@ -63,12 +69,16 @@ claude mcp add robloxstudio-mcp
63
69
  "mcpServers": {
64
70
  "robloxstudio-mcp": {
65
71
  "command": "npx",
66
- "args": ["-y", "@chrrxs/robloxstudio-mcp"]
72
+ "args": ["-y", "@chrrxs/robloxstudio-mcp@latest", "--auto-install-plugin"]
67
73
  }
68
74
  }
69
75
  }
70
76
  ```
71
77
 
78
+ `@latest` floats the server package to the newest npm release. `--auto-install-plugin` copies the matching `.rbxmx` bundled with that package into Studio's Plugins folder when the server starts.
79
+
80
+ If Studio shows a yellow plugin/server version mismatch banner, the connection remains usable. Restart the MCP server with `--auto-install-plugin`, then fully close and reopen Studio so it loads the matching plugin file.
81
+
72
82
  <details>
73
83
  <summary>Note for native Windows users</summary>
74
84
  If you encounter issues, you may need to run it through `cmd`. Update your configuration like this:
@@ -78,7 +88,7 @@ If you encounter issues, you may need to run it through `cmd`. Update your confi
78
88
  "mcpServers": {
79
89
  "robloxstudio-mcp": {
80
90
  "command": "cmd",
81
- "args": ["/c", "npx", "-y", "@chrrxs/robloxstudio-mcp@latest"]
91
+ "args": ["/c", "npx", "-y", "@chrrxs/robloxstudio-mcp@latest", "--auto-install-plugin"]
82
92
  }
83
93
  }
84
94
  }
@@ -106,6 +106,7 @@ local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "Scen
106
106
  local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
107
107
  local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
108
108
  local LuauExec = TS.import(script, script.Parent, "LuauExec")
109
+ local State = TS.import(script, script.Parent, "State")
109
110
  local StudioTestService = game:GetService("StudioTestService")
110
111
  -- Mirror of Communication.computeInstanceId() — duplicated here because the
111
112
  -- client broker runs in the play-server DM where it can't easily import from
@@ -201,6 +202,8 @@ local function reRegisterProxy(proxyId, role)
201
202
  placeName = resolvePlaceName(),
202
203
  dataModelName = game.Name,
203
204
  isRunning = RunService:IsRunning(),
205
+ pluginVersion = State.CURRENT_VERSION,
206
+ pluginVariant = State.PLUGIN_VARIANT,
204
207
  })
205
208
  end)
206
209
  end
@@ -456,6 +459,8 @@ local function registerProxy(player, rf)
456
459
  placeName = resolvePlaceName(),
457
460
  dataModelName = game.Name,
458
461
  isRunning = RunService:IsRunning(),
462
+ pluginVersion = State.CURRENT_VERSION,
463
+ pluginVariant = State.PLUGIN_VARIANT,
459
464
  })
460
465
  if not ok or not res or not res.Success then
461
466
  warn(`[robloxstudio-mcp] proxy register failed for {player.Name}`)
@@ -584,6 +589,8 @@ end
584
589
  local instanceId = computeInstanceId()
585
590
  local assignedRole
586
591
  local duplicateInstanceRole = false
592
+ local hasVersionMismatch = false
593
+ local lastVersionMismatchWarningKey
587
594
  -- Cache the published place name from MarketplaceService:GetProductInfo so
588
595
  -- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
589
596
  -- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
@@ -783,6 +790,8 @@ function sendReady(conn)
783
790
  placeName = resolvePlaceName(),
784
791
  dataModelName = game.Name,
785
792
  isRunning = RunService:IsRunning(),
793
+ pluginVersion = State.CURRENT_VERSION,
794
+ pluginVariant = State.PLUGIN_VARIANT,
786
795
  pluginReady = true,
787
796
  timestamp = tick(),
788
797
  }),
@@ -848,6 +857,23 @@ local function pollForRequests(connIndex)
848
857
  local mcpConnected = data.mcpConnected == true
849
858
  conn.lastHttpOk = true
850
859
  conn.lastMcpOk = mcpConnected
860
+ local _condition = data.serverVersion
861
+ if _condition == nil then
862
+ _condition = "unknown"
863
+ end
864
+ local serverVersion = _condition
865
+ if data.versionMismatch == true then
866
+ hasVersionMismatch = true
867
+ local warningKey = `{State.CURRENT_VERSION}:{serverVersion}`
868
+ if lastVersionMismatchWarningKey ~= warningKey then
869
+ lastVersionMismatchWarningKey = warningKey
870
+ warn(`[MCPPlugin] Version mismatch: Studio plugin v{State.CURRENT_VERSION} / MCP v{serverVersion}. Run npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin and restart Studio.`)
871
+ end
872
+ UI.showBanner("version-mismatch", `Plugin v{State.CURRENT_VERSION} / MCP v{serverVersion} mismatch`)
873
+ elseif hasVersionMismatch then
874
+ hasVersionMismatch = false
875
+ UI.hideBanner("version-mismatch")
876
+ end
851
877
  -- Server tells us when its in-memory instances map doesn't have us
852
878
  -- (e.g. after an MCP process restart). Re-issue /ready immediately so
853
879
  -- target=server/client-N start routing again. The throttle inside
@@ -860,12 +886,12 @@ local function pollForRequests(connIndex)
860
886
  local el = ui
861
887
  el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
862
888
  el.step1Label.Text = "HTTP server (OK)"
863
- local _condition = mcpConnected
864
- if _condition then
889
+ local _condition_1 = mcpConnected
890
+ if _condition_1 then
865
891
  local _value = (string.find(el.statusLabel.Text, "Connected"))
866
- _condition = not (_value ~= 0 and _value == _value and _value)
892
+ _condition_1 = not (_value ~= 0 and _value == _value and _value)
867
893
  end
868
- if _condition then
894
+ if _condition_1 then
869
895
  el.statusLabel.Text = "Connected"
870
896
  el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
871
897
  el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
@@ -896,11 +922,11 @@ local function pollForRequests(connIndex)
896
922
  conn.mcpWaitStartTime = tick()
897
923
  end
898
924
  local _exp = tick()
899
- local _condition_1 = conn.mcpWaitStartTime
900
- if _condition_1 == nil then
901
- _condition_1 = tick()
925
+ local _condition_2 = conn.mcpWaitStartTime
926
+ if _condition_2 == nil then
927
+ _condition_2 = tick()
902
928
  end
903
- local elapsed = _exp - _condition_1
929
+ local elapsed = _exp - _condition_2
904
930
  el.troubleshootLabel.Visible = elapsed > 8
905
931
  UI.startPulseAnimation()
906
932
  end
@@ -1109,11 +1135,9 @@ local function checkForUpdates()
1109
1135
  if _condition ~= "" and _condition then
1110
1136
  local latestVersion = data.version
1111
1137
  if Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0 then
1112
- local ui = UI.getElements()
1113
- ui.updateBannerText.Text = `v{latestVersion} available - github.com/chrrxs/robloxstudio-mcp`
1114
- ui.updateBanner.Visible = true
1115
- ui.contentFrame.Position = UDim2.new(0, 8, 0, 92)
1116
- ui.contentFrame.Size = UDim2.new(1, -16, 1, -100)
1138
+ if not hasVersionMismatch then
1139
+ UI.showBanner("update", `v{latestVersion} available - github.com/chrrxs/robloxstudio-mcp`)
1140
+ end
1117
1141
  end
1118
1142
  end
1119
1143
  end
@@ -1263,9 +1287,9 @@ local function computeBridgeStamp()
1263
1287
  for i = 1, #combined do
1264
1288
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1265
1289
  end
1266
- -- "2.15.0" is replaced with the package version at package time
1290
+ -- "2.15.1" is replaced with the package version at package time
1267
1291
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1268
- return `{tostring(h)}-2.15.0`
1292
+ return `{tostring(h)}-2.15.1`
1269
1293
  end
1270
1294
  local BRIDGE_STAMP = computeBridgeStamp()
1271
1295
  local function setSource(scriptInst, source)
@@ -7441,7 +7465,8 @@ return {
7441
7465
  <Properties>
7442
7466
  <string name="Name">State</string>
7443
7467
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
7444
- local CURRENT_VERSION = "2.15.0"
7468
+ local CURRENT_VERSION = "2.15.1"
7469
+ local PLUGIN_VARIANT = "inspector"
7445
7470
  local MAX_CONNECTIONS = 5
7446
7471
  local BASE_PORT = 58741
7447
7472
  local activeTabIndex = 0
@@ -7519,6 +7544,7 @@ local function getConnections()
7519
7544
  end
7520
7545
  return {
7521
7546
  CURRENT_VERSION = CURRENT_VERSION,
7547
+ PLUGIN_VARIANT = PLUGIN_VARIANT,
7522
7548
  MAX_CONNECTIONS = MAX_CONNECTIONS,
7523
7549
  BASE_PORT = BASE_PORT,
7524
7550
  connections = connections,
@@ -7691,6 +7717,7 @@ local buttonHover = false
7691
7717
  local toolbarButton
7692
7718
  local toolbarIcons
7693
7719
  local lastToolbarIcon
7720
+ local activeBannerKind
7694
7721
  local updateToolbarIcon
7695
7722
  local function setToolbarButton(btn, icons)
7696
7723
  toolbarButton = btn
@@ -7721,6 +7748,23 @@ local TWEEN_QUICK = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirect
7721
7748
  local function tweenProp(instance, props)
7722
7749
  TweenService:Create(instance, TWEEN_QUICK, props):Play()
7723
7750
  end
7751
+ local function showBanner(kind, text)
7752
+ activeBannerKind = kind
7753
+ elements.updateBannerText.Text = text
7754
+ elements.updateBanner.Visible = true
7755
+ elements.contentFrame.Position = UDim2.new(0, 8, 0, 92)
7756
+ elements.contentFrame.Size = UDim2.new(1, -16, 1, -100)
7757
+ end
7758
+ local function hideBanner(kind)
7759
+ if kind ~= nil and activeBannerKind ~= kind then
7760
+ return nil
7761
+ end
7762
+ activeBannerKind = nil
7763
+ elements.updateBanner.Visible = false
7764
+ elements.updateBannerText.Text = ""
7765
+ elements.contentFrame.Position = UDim2.new(0, 8, 0, 66)
7766
+ elements.contentFrame.Size = UDim2.new(1, -16, 1, -74)
7767
+ end
7724
7768
  local C = {
7725
7769
  bg = Color3.fromRGB(14, 14, 14),
7726
7770
  card = Color3.fromRGB(22, 22, 22),
@@ -8422,6 +8466,8 @@ return {
8422
8466
  startPulseAnimation = startPulseAnimation,
8423
8467
  setToolbarButton = setToolbarButton,
8424
8468
  updateToolbarIcon = updateToolbarIcon,
8469
+ showBanner = showBanner,
8470
+ hideBanner = hideBanner,
8425
8471
  getElements = function()
8426
8472
  return elements
8427
8473
  end,
@@ -106,6 +106,7 @@ local SceneAnalysisHandlers = TS.import(script, script.Parent, "handlers", "Scen
106
106
  local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
107
107
  local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
108
108
  local LuauExec = TS.import(script, script.Parent, "LuauExec")
109
+ local State = TS.import(script, script.Parent, "State")
109
110
  local StudioTestService = game:GetService("StudioTestService")
110
111
  -- Mirror of Communication.computeInstanceId() — duplicated here because the
111
112
  -- client broker runs in the play-server DM where it can't easily import from
@@ -201,6 +202,8 @@ local function reRegisterProxy(proxyId, role)
201
202
  placeName = resolvePlaceName(),
202
203
  dataModelName = game.Name,
203
204
  isRunning = RunService:IsRunning(),
205
+ pluginVersion = State.CURRENT_VERSION,
206
+ pluginVariant = State.PLUGIN_VARIANT,
204
207
  })
205
208
  end)
206
209
  end
@@ -456,6 +459,8 @@ local function registerProxy(player, rf)
456
459
  placeName = resolvePlaceName(),
457
460
  dataModelName = game.Name,
458
461
  isRunning = RunService:IsRunning(),
462
+ pluginVersion = State.CURRENT_VERSION,
463
+ pluginVariant = State.PLUGIN_VARIANT,
459
464
  })
460
465
  if not ok or not res or not res.Success then
461
466
  warn(`[robloxstudio-mcp] proxy register failed for {player.Name}`)
@@ -584,6 +589,8 @@ end
584
589
  local instanceId = computeInstanceId()
585
590
  local assignedRole
586
591
  local duplicateInstanceRole = false
592
+ local hasVersionMismatch = false
593
+ local lastVersionMismatchWarningKey
587
594
  -- Cache the published place name from MarketplaceService:GetProductInfo so
588
595
  -- /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
589
596
  -- from game.Name (the DataModel name, often "Place1" in edit). We only fetch
@@ -783,6 +790,8 @@ function sendReady(conn)
783
790
  placeName = resolvePlaceName(),
784
791
  dataModelName = game.Name,
785
792
  isRunning = RunService:IsRunning(),
793
+ pluginVersion = State.CURRENT_VERSION,
794
+ pluginVariant = State.PLUGIN_VARIANT,
786
795
  pluginReady = true,
787
796
  timestamp = tick(),
788
797
  }),
@@ -848,6 +857,23 @@ local function pollForRequests(connIndex)
848
857
  local mcpConnected = data.mcpConnected == true
849
858
  conn.lastHttpOk = true
850
859
  conn.lastMcpOk = mcpConnected
860
+ local _condition = data.serverVersion
861
+ if _condition == nil then
862
+ _condition = "unknown"
863
+ end
864
+ local serverVersion = _condition
865
+ if data.versionMismatch == true then
866
+ hasVersionMismatch = true
867
+ local warningKey = `{State.CURRENT_VERSION}:{serverVersion}`
868
+ if lastVersionMismatchWarningKey ~= warningKey then
869
+ lastVersionMismatchWarningKey = warningKey
870
+ warn(`[MCPPlugin] Version mismatch: Studio plugin v{State.CURRENT_VERSION} / MCP v{serverVersion}. Run npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin and restart Studio.`)
871
+ end
872
+ UI.showBanner("version-mismatch", `Plugin v{State.CURRENT_VERSION} / MCP v{serverVersion} mismatch`)
873
+ elseif hasVersionMismatch then
874
+ hasVersionMismatch = false
875
+ UI.hideBanner("version-mismatch")
876
+ end
851
877
  -- Server tells us when its in-memory instances map doesn't have us
852
878
  -- (e.g. after an MCP process restart). Re-issue /ready immediately so
853
879
  -- target=server/client-N start routing again. The throttle inside
@@ -860,12 +886,12 @@ local function pollForRequests(connIndex)
860
886
  local el = ui
861
887
  el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
862
888
  el.step1Label.Text = "HTTP server (OK)"
863
- local _condition = mcpConnected
864
- if _condition then
889
+ local _condition_1 = mcpConnected
890
+ if _condition_1 then
865
891
  local _value = (string.find(el.statusLabel.Text, "Connected"))
866
- _condition = not (_value ~= 0 and _value == _value and _value)
892
+ _condition_1 = not (_value ~= 0 and _value == _value and _value)
867
893
  end
868
- if _condition then
894
+ if _condition_1 then
869
895
  el.statusLabel.Text = "Connected"
870
896
  el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
871
897
  el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
@@ -896,11 +922,11 @@ local function pollForRequests(connIndex)
896
922
  conn.mcpWaitStartTime = tick()
897
923
  end
898
924
  local _exp = tick()
899
- local _condition_1 = conn.mcpWaitStartTime
900
- if _condition_1 == nil then
901
- _condition_1 = tick()
925
+ local _condition_2 = conn.mcpWaitStartTime
926
+ if _condition_2 == nil then
927
+ _condition_2 = tick()
902
928
  end
903
- local elapsed = _exp - _condition_1
929
+ local elapsed = _exp - _condition_2
904
930
  el.troubleshootLabel.Visible = elapsed > 8
905
931
  UI.startPulseAnimation()
906
932
  end
@@ -1109,11 +1135,9 @@ local function checkForUpdates()
1109
1135
  if _condition ~= "" and _condition then
1110
1136
  local latestVersion = data.version
1111
1137
  if Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0 then
1112
- local ui = UI.getElements()
1113
- ui.updateBannerText.Text = `v{latestVersion} available - github.com/chrrxs/robloxstudio-mcp`
1114
- ui.updateBanner.Visible = true
1115
- ui.contentFrame.Position = UDim2.new(0, 8, 0, 92)
1116
- ui.contentFrame.Size = UDim2.new(1, -16, 1, -100)
1138
+ if not hasVersionMismatch then
1139
+ UI.showBanner("update", `v{latestVersion} available - github.com/chrrxs/robloxstudio-mcp`)
1140
+ end
1117
1141
  end
1118
1142
  end
1119
1143
  end
@@ -1263,9 +1287,9 @@ local function computeBridgeStamp()
1263
1287
  for i = 1, #combined do
1264
1288
  h = (h * 33 + (string.byte(combined, i))) % 2147483647
1265
1289
  end
1266
- -- "2.15.0" is replaced with the package version at package time
1290
+ -- "2.15.1" is replaced with the package version at package time
1267
1291
  -- (scripts/build-plugin.mjs injectVersion), so a release bump also restamps.
1268
- return `{tostring(h)}-2.15.0`
1292
+ return `{tostring(h)}-2.15.1`
1269
1293
  end
1270
1294
  local BRIDGE_STAMP = computeBridgeStamp()
1271
1295
  local function setSource(scriptInst, source)
@@ -7441,7 +7465,8 @@ return {
7441
7465
  <Properties>
7442
7466
  <string name="Name">State</string>
7443
7467
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
7444
- local CURRENT_VERSION = "2.15.0"
7468
+ local CURRENT_VERSION = "2.15.1"
7469
+ local PLUGIN_VARIANT = "main"
7445
7470
  local MAX_CONNECTIONS = 5
7446
7471
  local BASE_PORT = 58741
7447
7472
  local activeTabIndex = 0
@@ -7519,6 +7544,7 @@ local function getConnections()
7519
7544
  end
7520
7545
  return {
7521
7546
  CURRENT_VERSION = CURRENT_VERSION,
7547
+ PLUGIN_VARIANT = PLUGIN_VARIANT,
7522
7548
  MAX_CONNECTIONS = MAX_CONNECTIONS,
7523
7549
  BASE_PORT = BASE_PORT,
7524
7550
  connections = connections,
@@ -7691,6 +7717,7 @@ local buttonHover = false
7691
7717
  local toolbarButton
7692
7718
  local toolbarIcons
7693
7719
  local lastToolbarIcon
7720
+ local activeBannerKind
7694
7721
  local updateToolbarIcon
7695
7722
  local function setToolbarButton(btn, icons)
7696
7723
  toolbarButton = btn
@@ -7721,6 +7748,23 @@ local TWEEN_QUICK = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirect
7721
7748
  local function tweenProp(instance, props)
7722
7749
  TweenService:Create(instance, TWEEN_QUICK, props):Play()
7723
7750
  end
7751
+ local function showBanner(kind, text)
7752
+ activeBannerKind = kind
7753
+ elements.updateBannerText.Text = text
7754
+ elements.updateBanner.Visible = true
7755
+ elements.contentFrame.Position = UDim2.new(0, 8, 0, 92)
7756
+ elements.contentFrame.Size = UDim2.new(1, -16, 1, -100)
7757
+ end
7758
+ local function hideBanner(kind)
7759
+ if kind ~= nil and activeBannerKind ~= kind then
7760
+ return nil
7761
+ end
7762
+ activeBannerKind = nil
7763
+ elements.updateBanner.Visible = false
7764
+ elements.updateBannerText.Text = ""
7765
+ elements.contentFrame.Position = UDim2.new(0, 8, 0, 66)
7766
+ elements.contentFrame.Size = UDim2.new(1, -16, 1, -74)
7767
+ end
7724
7768
  local C = {
7725
7769
  bg = Color3.fromRGB(14, 14, 14),
7726
7770
  card = Color3.fromRGB(22, 22, 22),
@@ -8422,6 +8466,8 @@ return {
8422
8466
  startPulseAnimation = startPulseAnimation,
8423
8467
  setToolbarButton = setToolbarButton,
8424
8468
  updateToolbarIcon = updateToolbarIcon,
8469
+ showBanner = showBanner,
8470
+ hideBanner = hideBanner,
8425
8471
  getElements = function()
8426
8472
  return elements
8427
8473
  end,
@@ -5,6 +5,7 @@ import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
5
5
  import CaptureHandlers from "./handlers/CaptureHandlers";
6
6
  import InputHandlers from "./handlers/InputHandlers";
7
7
  import LuauExec from "./LuauExec";
8
+ import State from "./State";
8
9
 
9
10
  interface StudioTestServiceMultiplayer extends StudioTestService {
10
11
  CanLeaveTest(): boolean;
@@ -134,6 +135,8 @@ function reRegisterProxy(proxyId: string, role: string): void {
134
135
  placeName: resolvePlaceName(),
135
136
  dataModelName: game.Name,
136
137
  isRunning: RunService.IsRunning(),
138
+ pluginVersion: State.CURRENT_VERSION,
139
+ pluginVariant: State.PLUGIN_VARIANT,
137
140
  }),
138
141
  );
139
142
  }
@@ -329,6 +332,8 @@ function registerProxy(player: Player, rf: RemoteFunction) {
329
332
  placeName: resolvePlaceName(),
330
333
  dataModelName: game.Name,
331
334
  isRunning: RunService.IsRunning(),
335
+ pluginVersion: State.CURRENT_VERSION,
336
+ pluginVariant: State.PLUGIN_VARIANT,
332
337
  });
333
338
  if (!ok || !res || !res.Success) {
334
339
  warn(`[robloxstudio-mcp] proxy register failed for ${player.Name}`);
@@ -47,6 +47,8 @@ function computeInstanceId(): string {
47
47
  const instanceId = computeInstanceId();
48
48
  let assignedRole: string | undefined;
49
49
  let duplicateInstanceRole = false;
50
+ let hasVersionMismatch = false;
51
+ let lastVersionMismatchWarningKey: string | undefined;
50
52
 
51
53
  // Cache the published place name from MarketplaceService:GetProductInfo so
52
54
  // /ready can carry a friendly identifier (e.g. "Natural Disasters") distinct
@@ -238,6 +240,8 @@ function sendReady(conn: Connection): void {
238
240
  placeName: resolvePlaceName(),
239
241
  dataModelName: game.Name,
240
242
  isRunning: RunService.IsRunning(),
243
+ pluginVersion: State.CURRENT_VERSION,
244
+ pluginVariant: State.PLUGIN_VARIANT,
241
245
  pluginReady: true,
242
246
  timestamp: tick(),
243
247
  }),
@@ -301,6 +305,19 @@ function pollForRequests(connIndex: number) {
301
305
  const mcpConnected = data.mcpConnected === true;
302
306
  conn.lastHttpOk = true;
303
307
  conn.lastMcpOk = mcpConnected;
308
+ const serverVersion = data.serverVersion ?? "unknown";
309
+ if (data.versionMismatch === true) {
310
+ hasVersionMismatch = true;
311
+ const warningKey = `${State.CURRENT_VERSION}:${serverVersion}`;
312
+ if (lastVersionMismatchWarningKey !== warningKey) {
313
+ lastVersionMismatchWarningKey = warningKey;
314
+ warn(`[MCPPlugin] Version mismatch: Studio plugin v${State.CURRENT_VERSION} / MCP v${serverVersion}. Run npx -y @chrrxs/robloxstudio-mcp@latest --auto-install-plugin and restart Studio.`);
315
+ }
316
+ UI.showBanner("version-mismatch", `Plugin v${State.CURRENT_VERSION} / MCP v${serverVersion} mismatch`);
317
+ } else if (hasVersionMismatch) {
318
+ hasVersionMismatch = false;
319
+ UI.hideBanner("version-mismatch");
320
+ }
304
321
 
305
322
  // Server tells us when its in-memory instances map doesn't have us
306
323
  // (e.g. after an MCP process restart). Re-issue /ready immediately so
@@ -539,11 +556,9 @@ function checkForUpdates() {
539
556
  if (ok && data?.version) {
540
557
  const latestVersion = data.version;
541
558
  if (Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0) {
542
- const ui = UI.getElements();
543
- ui.updateBannerText.Text = `v${latestVersion} available - github.com/chrrxs/robloxstudio-mcp`;
544
- ui.updateBanner.Visible = true;
545
- ui.contentFrame.Position = new UDim2(0, 8, 0, 92);
546
- ui.contentFrame.Size = new UDim2(1, -16, 1, -100);
559
+ if (!hasVersionMismatch) {
560
+ UI.showBanner("update", `v${latestVersion} available - github.com/chrrxs/robloxstudio-mcp`);
561
+ }
547
562
  }
548
563
  }
549
564
  }
@@ -1,6 +1,7 @@
1
1
  import { Connection } from "../types";
2
2
 
3
3
  const CURRENT_VERSION = "__VERSION__";
4
+ const PLUGIN_VARIANT = "__PLUGIN_VARIANT__";
4
5
  const MAX_CONNECTIONS = 5;
5
6
  const BASE_PORT = 58741;
6
7
  let activeTabIndex = 0;
@@ -81,6 +82,7 @@ function getConnections(): Connection[] {
81
82
 
82
83
  export = {
83
84
  CURRENT_VERSION,
85
+ PLUGIN_VARIANT,
84
86
  MAX_CONNECTIONS,
85
87
  BASE_PORT,
86
88
  connections,
@@ -38,6 +38,7 @@ interface ToolbarIcons {
38
38
  let toolbarButton: PluginToolbarButton | undefined;
39
39
  let toolbarIcons: ToolbarIcons | undefined;
40
40
  let lastToolbarIcon: string | undefined;
41
+ let activeBannerKind: string | undefined;
41
42
 
42
43
  function setToolbarButton(btn: PluginToolbarButton, icons: ToolbarIcons) {
43
44
  toolbarButton = btn;
@@ -77,6 +78,23 @@ function tweenProp(instance: Instance, props: Record<string, unknown>) {
77
78
  TweenService.Create(instance, TWEEN_QUICK, props as unknown as { [key: string]: unknown }).Play();
78
79
  }
79
80
 
81
+ function showBanner(kind: string, text: string) {
82
+ activeBannerKind = kind;
83
+ elements.updateBannerText.Text = text;
84
+ elements.updateBanner.Visible = true;
85
+ elements.contentFrame.Position = new UDim2(0, 8, 0, 92);
86
+ elements.contentFrame.Size = new UDim2(1, -16, 1, -100);
87
+ }
88
+
89
+ function hideBanner(kind?: string) {
90
+ if (kind !== undefined && activeBannerKind !== kind) return;
91
+ activeBannerKind = undefined;
92
+ elements.updateBanner.Visible = false;
93
+ elements.updateBannerText.Text = "";
94
+ elements.contentFrame.Position = new UDim2(0, 8, 0, 66);
95
+ elements.contentFrame.Size = new UDim2(1, -16, 1, -74);
96
+ }
97
+
80
98
  const C = {
81
99
  bg: Color3.fromRGB(14, 14, 14),
82
100
  card: Color3.fromRGB(22, 22, 22),
@@ -759,5 +777,7 @@ export = {
759
777
  startPulseAnimation,
760
778
  setToolbarButton,
761
779
  updateToolbarIcon,
780
+ showBanner,
781
+ hideBanner,
762
782
  getElements: () => elements,
763
783
  };
@@ -30,6 +30,10 @@ export interface RequestPayload {
30
30
 
31
31
  export interface PollResponse {
32
32
  mcpConnected: boolean;
33
+ serverVersion?: string;
34
+ pluginVersion?: string;
35
+ pluginVariant?: string;
36
+ versionMismatch?: boolean;
33
37
  request?: RequestPayload;
34
38
  requestId?: string;
35
39
  // Server signals knownInstance=false when its in-memory instances map
@@ -42,6 +46,8 @@ export interface ReadyResponse {
42
46
  success: boolean;
43
47
  assignedRole?: string;
44
48
  instanceId?: string;
49
+ serverVersion?: string;
50
+ versionMismatch?: boolean;
45
51
  error?: string;
46
52
  message?: string;
47
53
  }