@chrrxs/robloxstudio-mcp 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 +233 -22
- package/package.json +2 -2
- package/studio-plugin/INSTALLATION.md +13 -3
- package/studio-plugin/MCPInspectorPlugin.rbxmx +336 -34
- package/studio-plugin/MCPPlugin.rbxmx +336 -34
- package/studio-plugin/src/modules/ClientBroker.ts +12 -2
- package/studio-plugin/src/modules/Communication.ts +22 -5
- package/studio-plugin/src/modules/State.ts +2 -0
- package/studio-plugin/src/modules/UI.ts +20 -0
- package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -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
|
}
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
7345
|
+
log(`Removed conflicting ${otherAssetName}.`);
|
|
7189
7346
|
} catch (err) {
|
|
7190
|
-
|
|
7347
|
+
warn(`[install-plugin] Could not remove ${otherDest}: ${err}. Continuing.`);
|
|
7191
7348
|
}
|
|
7192
7349
|
return;
|
|
7193
7350
|
}
|
|
7194
|
-
|
|
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) => {
|
|
@@ -7288,9 +7447,11 @@ async function findDevRelease() {
|
|
|
7288
7447
|
}
|
|
7289
7448
|
return prerelease;
|
|
7290
7449
|
}
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7450
|
+
function prepareInstall({
|
|
7451
|
+
replaceVariant,
|
|
7452
|
+
log,
|
|
7453
|
+
warn
|
|
7454
|
+
}) {
|
|
7294
7455
|
const pluginsFolder = getPluginsFolder();
|
|
7295
7456
|
if (!existsSync3(pluginsFolder)) {
|
|
7296
7457
|
mkdirSync2(pluginsFolder, { recursive: true });
|
|
@@ -7298,18 +7459,56 @@ async function installPlugin() {
|
|
|
7298
7459
|
handleVariantConflict({
|
|
7299
7460
|
pluginsFolder,
|
|
7300
7461
|
otherAssetName: OTHER_VARIANT,
|
|
7301
|
-
replace: replaceVariant
|
|
7462
|
+
replace: replaceVariant,
|
|
7463
|
+
log,
|
|
7464
|
+
warn
|
|
7302
7465
|
});
|
|
7303
|
-
|
|
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...");
|
|
7304
7503
|
const release = dev ? await findDevRelease() : await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
7305
7504
|
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
7306
7505
|
if (!asset) {
|
|
7307
7506
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
7308
7507
|
}
|
|
7309
7508
|
const dest = join3(pluginsFolder, ASSET_NAME);
|
|
7310
|
-
|
|
7509
|
+
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
7311
7510
|
await download(asset.browser_download_url, dest);
|
|
7312
|
-
|
|
7511
|
+
log(`Installed to ${dest}`);
|
|
7313
7512
|
}
|
|
7314
7513
|
var REPO, ASSET_NAME, OTHER_VARIANT, TIMEOUT_MS, MAX_REDIRECTS;
|
|
7315
7514
|
var init_install_plugin = __esm({
|
|
@@ -7334,6 +7533,18 @@ if (process.argv.includes("--install-plugin")) {
|
|
|
7334
7533
|
process.exitCode = 1;
|
|
7335
7534
|
});
|
|
7336
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
|
+
}
|
|
7337
7548
|
const flagValue = (flag) => {
|
|
7338
7549
|
const idx = process.argv.indexOf(flag);
|
|
7339
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.
|
|
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
|
}
|