@chrrxs/robloxstudio-mcp 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 +125 -19
- package/package.json +2 -2
- package/studio-plugin/INSTALLATION.md +13 -3
- package/studio-plugin/MCPInspectorPlugin.rbxmx +62 -16
- package/studio-plugin/MCPPlugin.rbxmx +62 -16
- package/studio-plugin/src/modules/ClientBroker.ts +5 -0
- package/studio-plugin/src/modules/Communication.ts +20 -5
- package/studio-plugin/src/modules/State.ts +2 -0
- package/studio-plugin/src/modules/UI.ts +20 -0
- package/studio-plugin/src/types/index.d.ts +6 -0
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
7345
|
+
log(`Removed conflicting ${otherAssetName}.`);
|
|
7294
7346
|
} catch (err) {
|
|
7295
|
-
|
|
7347
|
+
warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
|
|
7296
7348
|
}
|
|
7297
7349
|
return;
|
|
7298
7350
|
}
|
|
7299
|
-
|
|
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) => {
|
|
@@ -7393,9 +7447,11 @@ async function findDevRelease() {
|
|
|
7393
7447
|
}
|
|
7394
7448
|
return prerelease;
|
|
7395
7449
|
}
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7450
|
+
function prepareInstall({
|
|
7451
|
+
replaceVariant,
|
|
7452
|
+
log,
|
|
7453
|
+
warn
|
|
7454
|
+
}) {
|
|
7399
7455
|
const pluginsFolder = getPluginsFolder();
|
|
7400
7456
|
if (!existsSync3(pluginsFolder)) {
|
|
7401
7457
|
mkdirSync2(pluginsFolder, { recursive: true });
|
|
@@ -7403,18 +7459,56 @@ async function installPlugin() {
|
|
|
7403
7459
|
handleVariantConflict({
|
|
7404
7460
|
pluginsFolder,
|
|
7405
7461
|
otherAssetName: OTHER_VARIANT,
|
|
7406
|
-
replace: replaceVariant
|
|
7462
|
+
replace: replaceVariant,
|
|
7463
|
+
log,
|
|
7464
|
+
warn
|
|
7407
7465
|
});
|
|
7408
|
-
|
|
7466
|
+
return pluginsFolder;
|
|
7467
|
+
}
|
|
7468
|
+
function bundledAssetPath() {
|
|
7469
|
+
const currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
7470
|
+
const candidates = [
|
|
7471
|
+
join3(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
7472
|
+
join3(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
7473
|
+
];
|
|
7474
|
+
return candidates.find((candidate) => existsSync3(candidate)) ?? null;
|
|
7475
|
+
}
|
|
7476
|
+
function filesMatch(a, b) {
|
|
7477
|
+
if (!existsSync3(b)) return false;
|
|
7478
|
+
const aBytes = readFileSync3(a);
|
|
7479
|
+
const bBytes = readFileSync3(b);
|
|
7480
|
+
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
7481
|
+
}
|
|
7482
|
+
async function installBundledPlugin(options = {}) {
|
|
7483
|
+
const log = options.log ?? console.log;
|
|
7484
|
+
const warn = options.warn ?? console.warn;
|
|
7485
|
+
const replaceVariant = options.replaceVariant ?? process.argv.includes("--replace-variant");
|
|
7486
|
+
const source = bundledAssetPath();
|
|
7487
|
+
if (!source) {
|
|
7488
|
+
throw new Error(`Bundled ${ASSET_NAME} not found in package`);
|
|
7489
|
+
}
|
|
7490
|
+
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
7491
|
+
const dest = join3(pluginsFolder, ASSET_NAME);
|
|
7492
|
+
if (filesMatch(source, dest)) return;
|
|
7493
|
+
copyFileSync(source, dest);
|
|
7494
|
+
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
7495
|
+
}
|
|
7496
|
+
async function installPlugin(options = {}) {
|
|
7497
|
+
const dev = options.dev ?? process.argv.includes("--dev");
|
|
7498
|
+
const replaceVariant = options.replaceVariant ?? process.argv.includes("--replace-variant");
|
|
7499
|
+
const log = options.log ?? console.log;
|
|
7500
|
+
const warn = options.warn ?? console.warn;
|
|
7501
|
+
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
7502
|
+
log(dev ? "Fetching latest dev prerelease..." : "Fetching latest release...");
|
|
7409
7503
|
const release = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
7410
7504
|
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
7411
7505
|
if (!asset) {
|
|
7412
7506
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
7413
7507
|
}
|
|
7414
7508
|
const dest = join3(pluginsFolder, ASSET_NAME);
|
|
7415
|
-
|
|
7509
|
+
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
7416
7510
|
await download(asset.browser_download_url, dest);
|
|
7417
|
-
|
|
7511
|
+
log(`Installed to ${dest}`);
|
|
7418
7512
|
}
|
|
7419
7513
|
var REPO, ASSET_NAME, OTHER_VARIANT, TIMEOUT_MS, MAX_REDIRECTS;
|
|
7420
7514
|
var init_install_plugin = __esm({
|
|
@@ -7439,6 +7533,18 @@ if (process.argv.includes("--install-plugin")) {
|
|
|
7439
7533
|
process.exitCode = 1;
|
|
7440
7534
|
});
|
|
7441
7535
|
} else {
|
|
7536
|
+
if (process.argv.includes("--auto-install-plugin")) {
|
|
7537
|
+
const { installBundledPlugin: installBundledPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
|
|
7538
|
+
await installBundledPlugin2({
|
|
7539
|
+
replaceVariant: process.argv.includes("--replace-variant"),
|
|
7540
|
+
log: (message) => console.error(`[install-plugin] ${message}`),
|
|
7541
|
+
warn: (message) => console.error(message)
|
|
7542
|
+
}).catch((err) => {
|
|
7543
|
+
console.error(
|
|
7544
|
+
`[install-plugin] Auto-install skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
7545
|
+
);
|
|
7546
|
+
});
|
|
7547
|
+
}
|
|
7442
7548
|
const flagValue = (flag) => {
|
|
7443
7549
|
const idx = process.argv.indexOf(flag);
|
|
7444
7550
|
return idx !== -1 && idx + 1 < process.argv.length ? process.argv[idx + 1] : void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrrxs/robloxstudio-mcp",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.1",
|
|
4
4
|
"description": "MCP server for testing, debugging, and controlling Roblox Studio from AI coding tools",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/chrrxs/robloxstudio-mcp.git"
|
|
31
|
+
"url": "git+https://github.com/chrrxs/robloxstudio-mcp.git"
|
|
32
32
|
},
|
|
33
33
|
"homepage": "https://github.com/chrrxs/robloxstudio-mcp#readme",
|
|
34
34
|
"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
|
|
864
|
-
if
|
|
889
|
+
local _condition_1 = mcpConnected
|
|
890
|
+
if _condition_1 then
|
|
865
891
|
local _value = (string.find(el.statusLabel.Text, "Connected"))
|
|
866
|
-
|
|
892
|
+
_condition_1 = not (_value ~= 0 and _value == _value and _value)
|
|
867
893
|
end
|
|
868
|
-
if
|
|
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
|
|
900
|
-
if
|
|
901
|
-
|
|
925
|
+
local _condition_2 = conn.mcpWaitStartTime
|
|
926
|
+
if _condition_2 == nil then
|
|
927
|
+
_condition_2 = tick()
|
|
902
928
|
end
|
|
903
|
-
local elapsed = _exp -
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
864
|
-
if
|
|
889
|
+
local _condition_1 = mcpConnected
|
|
890
|
+
if _condition_1 then
|
|
865
891
|
local _value = (string.find(el.statusLabel.Text, "Connected"))
|
|
866
|
-
|
|
892
|
+
_condition_1 = not (_value ~= 0 and _value == _value and _value)
|
|
867
893
|
end
|
|
868
|
-
if
|
|
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
|
|
900
|
-
if
|
|
901
|
-
|
|
925
|
+
local _condition_2 = conn.mcpWaitStartTime
|
|
926
|
+
if _condition_2 == nil then
|
|
927
|
+
_condition_2 = tick()
|
|
902
928
|
end
|
|
903
|
-
local elapsed = _exp -
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
}
|