@chrrxs/robloxstudio-mcp-inspector 2.14.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
  }
@@ -721,6 +773,7 @@ var init_http_server = __esm({
721
773
  simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.text, body.target, body.instance_id),
722
774
  character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target, body.instance_id),
723
775
  get_memory_breakdown: (tools, body) => tools.getMemoryBreakdown(body.target, body.tags, body.instance_id),
776
+ get_scene_analysis: (tools, body) => tools.getSceneAnalysis(body.mode, body.target, body.topN, body.raw, body.instance_id),
724
777
  export_rbxm: (tools, body) => tools.exportRbxm(body.instance_paths, body.output_path, body.target, body.instance_id),
725
778
  import_rbxm: (tools, body) => tools.importRbxm(body.source, body.parent_path, body.target, body.instance_id),
726
779
  find_and_replace_in_scripts: (tools, body) => tools.findAndReplaceInScripts(body.pattern, body.replacement, {
@@ -2884,6 +2937,19 @@ var init_tools = __esm({
2884
2937
  const clientCount = this._clientRolesForInstance(instanceId).length;
2885
2938
  return { ok: false, roles, timedOut: true, extraClients: clientCount > expectedClientCount, clientCount };
2886
2939
  }
2940
+ async _waitForRuntimeRolesFresh(instanceId, connectedAfter, requiredRoles, timeoutSec = 60) {
2941
+ const deadline = Date.now() + timeoutSec * 1e3;
2942
+ while (Date.now() < deadline) {
2943
+ const instances = this.bridge.getInstances().filter((i) => i.instanceId === instanceId);
2944
+ const roles = instances.map((i) => i.role);
2945
+ const freshRoles = new Set(instances.filter((i) => i.connectedAt >= connectedAfter).map((i) => i.role));
2946
+ if (requiredRoles.every((role) => freshRoles.has(role))) {
2947
+ return { ok: true, roles, timedOut: false };
2948
+ }
2949
+ await sleep(250);
2950
+ }
2951
+ return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
2952
+ }
2887
2953
  async getFileTree(path2 = "", instance_id) {
2888
2954
  const response = await this._callSingle("/api/file-tree", { path: path2 }, void 0, instance_id);
2889
2955
  return {
@@ -3532,12 +3598,37 @@ ${code}`
3532
3598
  throw new Error("start_playtest is single-player only. Use multiplayer_test_start for multi-client StudioTestService sessions.");
3533
3599
  }
3534
3600
  const data = { mode };
3535
- const response = await this._callSingle("/api/start-playtest", data, void 0, instance_id);
3601
+ const startedAt = Date.now();
3602
+ const resolved = this.bridge.resolveTarget({ instance_id, target: void 0 });
3603
+ if (!resolved.ok)
3604
+ throw new RoutingFailure(resolved.error);
3605
+ if (resolved.mode !== "single") {
3606
+ throw new RoutingFailure({
3607
+ code: "target_role_not_present_on_instance",
3608
+ message: "This tool does not support target=all. Pick a specific role or omit target.",
3609
+ data: {
3610
+ instances: this.bridge.getPublicInstances(),
3611
+ count: this.bridge.getInstances().length
3612
+ }
3613
+ });
3614
+ }
3615
+ const response = await this.client.request("/api/start-playtest", data, resolved.targetInstanceId, resolved.targetRole);
3616
+ let wait;
3617
+ if (response?.success === true) {
3618
+ const requiredRoles = mode === "play" ? ["server", "client-1"] : ["server"];
3619
+ wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles);
3620
+ }
3621
+ const body = wait ? {
3622
+ ...response,
3623
+ runtimeReady: wait.ok,
3624
+ timedOut: wait.timedOut,
3625
+ roles: wait.roles
3626
+ } : response;
3536
3627
  return {
3537
3628
  content: [
3538
3629
  {
3539
3630
  type: "text",
3540
- text: JSON.stringify(response)
3631
+ text: JSON.stringify(body)
3541
3632
  }
3542
3633
  ]
3543
3634
  };
@@ -4602,6 +4693,39 @@ ${code}`
4602
4693
  }
4603
4694
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
4604
4695
  }
4696
+ async getSceneAnalysis(mode, target, topN, raw, instance_id) {
4697
+ const tgt = target ?? "all";
4698
+ const data = {};
4699
+ if (mode !== void 0)
4700
+ data.mode = mode;
4701
+ if (topN !== void 0)
4702
+ data.topN = topN;
4703
+ if (raw !== void 0)
4704
+ data.raw = raw;
4705
+ const resolved = this.bridge.resolveTarget({ instance_id, target: tgt });
4706
+ if (!resolved.ok)
4707
+ throw new RoutingFailure(resolved.error);
4708
+ if (resolved.mode === "single") {
4709
+ const response = await this.client.request("/api/get-scene-analysis", data, resolved.targetInstanceId, resolved.targetRole);
4710
+ return { content: [{ type: "text", text: JSON.stringify(response) }] };
4711
+ }
4712
+ const targets = resolved.targets.filter((t) => t.targetRole !== "edit-proxy");
4713
+ const responses = await Promise.allSettled(targets.map(async (t) => ({
4714
+ peer: t.targetRole,
4715
+ result: await this.client.request("/api/get-scene-analysis", data, t.targetInstanceId, t.targetRole)
4716
+ })));
4717
+ const body = {};
4718
+ for (let i = 0; i < responses.length; i++) {
4719
+ const r = responses[i];
4720
+ const peer = targets[i].targetRole;
4721
+ if (r.status === "fulfilled") {
4722
+ body[peer] = r.value.result;
4723
+ } else {
4724
+ body[peer] = { error: "disconnected" };
4725
+ }
4726
+ }
4727
+ return { content: [{ type: "text", text: JSON.stringify(body) }] };
4728
+ }
4605
4729
  async exportRbxm(instancePaths, outputPath, target, instance_id) {
4606
4730
  if (!Array.isArray(instancePaths) || instancePaths.length === 0) {
4607
4731
  throw new Error("instance_paths must be a non-empty array for export_rbxm");
@@ -6032,7 +6156,7 @@ var init_definitions = __esm({
6032
6156
  {
6033
6157
  name: "start_playtest",
6034
6158
  category: "write",
6035
- description: "Start a simple single-player Studio playtest in play or run mode. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
6159
+ description: "Start a simple single-player Studio playtest in play or run mode, waiting until a runtime peer registers with MCP. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
6036
6160
  inputSchema: {
6037
6161
  type: "object",
6038
6162
  properties: {
@@ -7010,6 +7134,39 @@ part(0,2,0,2,1,1,"b")`,
7010
7134
  }
7011
7135
  }
7012
7136
  },
7137
+ {
7138
+ name: "get_scene_analysis",
7139
+ category: "read",
7140
+ description: 'Read Roblox SceneAnalysisService data for attribution-focused performance analysis. Complements get_memory_breakdown: returns compact top-N entries for instance composition, script memory, unparented instances, triangle composition, animation memory, and audio memory. Requires the Studio Scene Analysis beta feature; if disabled, returns scene_analysis_not_enabled with betaFeatureRequired=true. target="all" (default) returns per-peer data; single-peer targets return that peer directly. raw=true includes the full nested Scene Analysis tree.',
7141
+ inputSchema: {
7142
+ type: "object",
7143
+ properties: {
7144
+ mode: {
7145
+ type: "string",
7146
+ enum: ["all", "instance_composition", "script_memory", "unparented_instances", "triangle_composition", "animation_memory", "audio_memory"],
7147
+ description: 'Scene analysis mode to read. Defaults to "all".'
7148
+ },
7149
+ target: {
7150
+ type: "string",
7151
+ description: 'Peer to read from: "edit", "server", "client-N", or "all" (default).'
7152
+ },
7153
+ topN: {
7154
+ type: "number",
7155
+ minimum: 1,
7156
+ maximum: 100,
7157
+ description: "Number of flattened top entries to include per mode. Defaults to 10; plugin clamps to 1-100."
7158
+ },
7159
+ raw: {
7160
+ type: "boolean",
7161
+ description: "Include the full nested SceneAnalysisService tree in each mode result. Defaults to false."
7162
+ },
7163
+ instance_id: {
7164
+ type: "string",
7165
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7166
+ }
7167
+ }
7168
+ }
7169
+ },
7013
7170
  // === SerializationService round-trip ===
7014
7171
  {
7015
7172
  name: "export_rbxm",
@@ -7178,20 +7335,20 @@ function getPluginsFolder() {
7178
7335
  }
7179
7336
  return join2(homedir2(), "Documents", "Roblox", "Plugins");
7180
7337
  }
7181
- function handleVariantConflict({ pluginsFolder, otherAssetName, replace }) {
7338
+ function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
7182
7339
  const otherDest = join2(pluginsFolder, otherAssetName);
7183
7340
  if (!existsSync2(otherDest))
7184
7341
  return;
7185
7342
  if (replace) {
7186
7343
  try {
7187
7344
  unlinkSync(otherDest);
7188
- console.log(`Removed conflicting ${otherAssetName}.`);
7345
+ log(`Removed conflicting ${otherAssetName}.`);
7189
7346
  } catch (err) {
7190
- console.warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
7347
+ warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
7191
7348
  }
7192
7349
  return;
7193
7350
  }
7194
- console.warn(`
7351
+ warn(`
7195
7352
  [install-plugin] WARNING: ${otherAssetName} is already present in ${pluginsFolder}.
7196
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.
7197
7354
  Re-run with --replace-variant to remove ${otherAssetName}, or delete it manually.
@@ -7222,10 +7379,12 @@ var init_dist = __esm({
7222
7379
  // src/install-plugin.ts
7223
7380
  var install_plugin_exports = {};
7224
7381
  __export(install_plugin_exports, {
7382
+ installBundledPlugin: () => installBundledPlugin,
7225
7383
  installPlugin: () => installPlugin
7226
7384
  });
7227
- import { createWriteStream, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
7228
- 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";
7229
7388
  import { get } from "https";
7230
7389
  function httpsGet(url) {
7231
7390
  return new Promise((resolve2, reject) => {
@@ -7278,8 +7437,11 @@ async function fetchJson(url) {
7278
7437
  }
7279
7438
  return JSON.parse(Buffer.concat(chunks).toString());
7280
7439
  }
7281
- async function installPlugin() {
7282
- const replaceVariant = process.argv.includes("--replace-variant");
7440
+ function prepareInstall({
7441
+ replaceVariant,
7442
+ log,
7443
+ warn
7444
+ }) {
7283
7445
  const pluginsFolder = getPluginsFolder();
7284
7446
  if (!existsSync3(pluginsFolder)) {
7285
7447
  mkdirSync2(pluginsFolder, { recursive: true });
@@ -7287,18 +7449,55 @@ async function installPlugin() {
7287
7449
  handleVariantConflict({
7288
7450
  pluginsFolder,
7289
7451
  otherAssetName: OTHER_VARIANT,
7290
- replace: replaceVariant
7452
+ replace: replaceVariant,
7453
+ log,
7454
+ warn
7291
7455
  });
7292
- 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...");
7293
7492
  const release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
7294
7493
  const asset = release.assets?.find((a) => a.name === ASSET_NAME);
7295
7494
  if (!asset) {
7296
7495
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
7297
7496
  }
7298
7497
  const dest = join3(pluginsFolder, ASSET_NAME);
7299
- console.log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
7498
+ log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
7300
7499
  await download(asset.browser_download_url, dest);
7301
- console.log(`Installed to ${dest}`);
7500
+ log(`Installed to ${dest}`);
7302
7501
  }
7303
7502
  var REPO, ASSET_NAME, OTHER_VARIANT, TIMEOUT_MS, MAX_REDIRECTS;
7304
7503
  var init_install_plugin = __esm({
@@ -7323,6 +7522,18 @@ if (process.argv.includes("--install-plugin")) {
7323
7522
  process.exitCode = 1;
7324
7523
  });
7325
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
+ }
7326
7537
  const require2 = createRequire(import.meta.url);
7327
7538
  const { version: VERSION } = require2("../package.json");
7328
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.14.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
  }