@el4cteo/rbx-studio-mcp 0.4.1 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@el4cteo/rbx-studio-mcp",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "MCP server for Roblox Studio. 29 tools, push-based SSE bridge, editor-safe script edits, one-step undo.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,11 +45,11 @@
45
45
  "ai-agent"
46
46
  ],
47
47
  "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.22.0",
49
- "zod": "^4.4.3"
48
+ "@modelcontextprotocol/sdk": "^1.30.0",
49
+ "zod": "^4.5.4"
50
50
  },
51
51
  "devDependencies": {
52
- "@types/node": "^26.2.0",
52
+ "@types/node": "^26.4.1",
53
53
  "typescript": "^7.0.2"
54
54
  },
55
55
  "author": "EL4CTEO",
@@ -9,7 +9,7 @@
9
9
 
10
10
  local Config = {}
11
11
 
12
- Config.PLUGIN_VERSION = "0.4.1"
12
+ Config.PLUGIN_VERSION = "0.4.2"
13
13
 
14
14
  -- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
15
15
  -- computes the same hash from its own copy of the sources and compares, so a
@@ -37,6 +37,14 @@ local Api = require(script.handlers.Api)
37
37
  local Scripts = require(script.handlers.Scripts)
38
38
  local Session = require(script.handlers.Session)
39
39
 
40
+ --[[
41
+ How long a closed panel has to stay closed before that is written down.
42
+
43
+ Long enough to outlast Studio tearing a place down, short enough that a user
44
+ who closes the panel and quits immediately still has it remembered.
45
+ ]]
46
+ local WIDGET_CLOSE_GRACE = 1
47
+
40
48
  local SETTING_PORT = "port"
41
49
  local SETTING_AUTOCONNECT = "autoConnect"
42
50
  local SETTING_FORCE_POLL = "forcePoll"
@@ -243,10 +251,40 @@ if RunService:IsEdit() then
243
251
  unloading = true
244
252
  end)
245
253
 
254
+ --[[
255
+ Only a close the user actually asked for is remembered.
256
+
257
+ Opening is recorded at once -- there is no teardown that opens a panel,
258
+ so a `true` is always the user. A `false` is not: Studio disables the
259
+ widget on its way out of a place, and `plugin.Unloading` is not
260
+ guaranteed to have fired by then, so the `unloading` flag alone lost the
261
+ race often enough that people found the panel closed on their next
262
+ launch with no memory of closing it. That is the "it does not open any
263
+ more" report, and it was self-inflicted and permanent -- once `false` is
264
+ stored, nothing writes `true` again until the panel is opened by hand.
265
+
266
+ So a close has to survive a moment before it counts. A teardown never
267
+ does: the DataModel is gone long before the delay is up, taking the
268
+ pending write with it. A user clicking the X does, and a user who
269
+ re-opens within the delay cancels it, which is also correct.
270
+ ]]
271
+ local closeGeneration = 0
246
272
  widget:GetPropertyChangedSignal("Enabled"):Connect(function()
247
- if not unloading then
248
- plugin:SetSetting(SETTING_WIDGET_OPEN, widget.Enabled)
273
+ if unloading then
274
+ return
275
+ end
276
+ closeGeneration += 1
277
+ if widget.Enabled then
278
+ plugin:SetSetting(SETTING_WIDGET_OPEN, true)
279
+ return
249
280
  end
281
+ local generation = closeGeneration
282
+ task.delay(WIDGET_CLOSE_GRACE, function()
283
+ if unloading or generation ~= closeGeneration or widget.Enabled then
284
+ return
285
+ end
286
+ plugin:SetSetting(SETTING_WIDGET_OPEN, false)
287
+ end)
250
288
  end)
251
289
 
252
290
  -- Only a real, sane size, and only while the panel is up: a hidden or
@@ -7,7 +7,7 @@
7
7
  * Usage: node scripts/install-plugin.mjs
8
8
  */
9
9
  import { execFileSync } from "node:child_process";
10
- import { copyFileSync, existsSync, mkdirSync } from "node:fs";
10
+ import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { dirname, join, resolve } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
@@ -36,4 +36,27 @@ execFileSync(process.execPath, [join(root, "scripts", "build-plugin.mjs")], { st
36
36
 
37
37
  const target = pluginsDir();
38
38
  if (!existsSync(target)) mkdirSync(target, { recursive: true });
39
- copyFileSync(built, join(target, "StudioMCP.rbxmx"));
39
+
40
+ /*
41
+ * Copied beside the target and renamed over it, never written in place.
42
+ *
43
+ * Studio watches this directory and reloads the moment the file changes, so an
44
+ * in-place copy hands it whatever is on disk at that instant -- which, for a
45
+ * file of this size, is regularly half of one build and none of the next. The
46
+ * plugin then fails to parse and Studio drops it silently: no toolbar button,
47
+ * nothing in the plugin list, and no error saying why. It reads as the plugin
48
+ * randomly not being installed, and re-running the install "fixes" it, which is
49
+ * exactly what you would expect from a race.
50
+ *
51
+ * Rename within one directory is atomic, so Studio sees the old file or the new
52
+ * one and never the seam between them.
53
+ */
54
+ const destination = join(target, "StudioMCP.rbxmx");
55
+ const staged = join(target, "StudioMCP.rbxmx.incoming");
56
+ try {
57
+ copyFileSync(built, staged);
58
+ renameSync(staged, destination);
59
+ } catch (cause) {
60
+ rmSync(staged, { force: true });
61
+ throw cause;
62
+ }