@el4cteo/rbx-studio-mcp 0.8.3 → 0.8.4

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/README.md CHANGED
@@ -108,7 +108,7 @@ Two things to watch: a playtest connects a second session, so pass `studioId` an
108
108
 
109
109
  ## The console panel
110
110
 
111
- Every call is logged with how long it took. At the foot of the panel is a command line — type a command, or type a sentence and a coding agent answers it.
111
+ Every call is logged with how long it took. At the foot of the panel is a command line. Type a command — or run `chat on` and type a sentence to have a coding agent answer it.
112
112
 
113
113
  | | |
114
114
  |---|---|
@@ -119,12 +119,13 @@ Every call is logged with how long it took. At the foot of the panel is a comman
119
119
  | `theme [name]` `visuals` `autoopen [on\|off]` `log [level]` `clear` `copy` | the panel |
120
120
  | `port [n]` `reconnect` | the connection |
121
121
  | `cloud [key\|user\|group\|test\|forget]` | the Open Cloud key `upload` uses |
122
+ | `chat [on\|off]` | let an agent answer sentences (off by default) |
122
123
  | `agent [use <id>\|new]` `stop` | which agent runs your prompts |
123
- | anything else | sent to that agent |
124
+ | anything else | sent to that agent, once `chat` is on |
124
125
 
125
126
  Click the bar and every command is listed with what it does. Keep typing to filter, scroll for the rest, click one to fill it in.
126
127
 
127
- **Prompts start a real agent** — whichever you have on PATH: Claude Code, Codex, opencode, Gemini, Cursor, Amp, Qwen Code, Factory Droid, goose, Copilot CLI, Aider, Crush, DeepSeek Harness. It runs headless, drives the same Studio, and its work appears in the log. It is a separate session from your terminal, billed separately, and allowed the `rbx-studio` tools only. `stop` cancels it.
128
+ **With `chat on`, prompts start a real agent** — whichever you have on PATH: Claude Code, Codex, opencode, Gemini, Cursor, Amp, Qwen Code, Factory Droid, goose, Copilot CLI, Aider, Crush, DeepSeek Harness. It runs headless, drives the same Studio, and its work appears in the log. It is a separate session from your terminal, billed separately, and allowed the `rbx-studio` tools only. `stop` cancels it.
128
129
 
129
130
  Eight themes behind the tab on the right edge. Your pick is remembered.
130
131
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@el4cteo/rbx-studio-mcp",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "MCP server for Roblox Studio. 34 tools, push-based SSE bridge, editor-safe script edits, one-step undo.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,6 +46,9 @@ export type Hooks = {
46
46
  -- Whether the panel may open itself on load, and the setter for it.
47
47
  autoOpen: () -> boolean,
48
48
  saveAutoOpen: (boolean) -> (),
49
+ -- Whether a line that is not a command is sent to an agent, and the setter.
50
+ chat: () -> boolean,
51
+ saveChat: (boolean) -> (),
49
52
  -- The transport's own view of the connection, which the console only ever
50
53
  -- receives second-hand as a string to display.
51
54
  status: () -> string,
@@ -137,11 +140,16 @@ define({
137
140
  for _, entry in entries do
138
141
  Console.log("dim", " " .. entry.usage, entry.summary)
139
142
  end
140
- Console.log(
141
- "dim",
142
- " anything else",
143
- "sent to a coding agent -- run `agent` to see which"
144
- )
143
+ local current = hooks
144
+ if current ~= nil and current.chat() then
145
+ Console.log(
146
+ "dim",
147
+ " anything else",
148
+ "sent to a coding agent -- run `agent` to see which"
149
+ )
150
+ else
151
+ Console.log("dim", " anything else", "ignored -- `chat on` sends it to a coding agent")
152
+ end
145
153
  end,
146
154
  })
147
155
 
@@ -334,6 +342,56 @@ define({
334
342
  end,
335
343
  })
336
344
 
345
+ --[[
346
+ Whether a sentence typed at the prompt reaches an agent.
347
+
348
+ Off by default. A prompt starts a real agent -- a separate session, billed
349
+ separately -- so a line typed into the panel by habit, or pasted by mistake,
350
+ should not spend anything until the user has said they want that. Commands
351
+ are unaffected either way, and so are `agent` and `stop`.
352
+ ]]
353
+ define({
354
+ name = "chat",
355
+ usage = "chat [on|off]",
356
+ summary = "let a coding agent answer sentences typed here; OFF by default",
357
+ run = function(args)
358
+ local current = hooks
359
+ if current == nil then
360
+ return
361
+ end
362
+
363
+ local wanted = args[1]
364
+ if #args > 1 or (wanted ~= nil and wanted ~= "on" and wanted ~= "off") then
365
+ Console.log("error", "usage: chat [on|off]")
366
+ return
367
+ end
368
+
369
+ if wanted == nil then
370
+ Console.log(
371
+ "dim",
372
+ if current.chat()
373
+ then "chat on -- a sentence is sent to a coding agent"
374
+ else "chat off -- a sentence is ignored; `chat on` to let an agent answer"
375
+ )
376
+ return
377
+ end
378
+
379
+ local value = wanted == "on"
380
+ if value == current.chat() then
381
+ Console.log("dim", string.format("already chat %s", wanted))
382
+ return
383
+ end
384
+
385
+ current.saveChat(value)
386
+ Console.setPromptChat(value)
387
+ if value then
388
+ Console.log("ok", "chat on", "type a sentence and a coding agent answers; `agent` shows which")
389
+ else
390
+ Console.log("ok", "chat off", "sentences are ignored; commands still run")
391
+ end
392
+ end,
393
+ })
394
+
337
395
  define({
338
396
  name = "place",
339
397
  usage = "place",
@@ -648,6 +706,15 @@ function Commands.run(line: string)
648
706
  return
649
707
  end
650
708
  end
709
+ local current = hooks
710
+ if current == nil or not current.chat() then
711
+ Console.log(
712
+ "dim",
713
+ "not a command, and chat is off -- nothing was sent",
714
+ "run `chat on` to let a coding agent answer sentences, or `help` for commands"
715
+ )
716
+ return
717
+ end
651
718
  Console.setPromptBusy(true)
652
719
  remote("prompt", words, trimmed)
653
720
  return
@@ -1,65 +1,65 @@
1
- --!strict
2
- --[[
3
- Connection settings for the Studio MCP plugin.
4
-
5
- The port must match the one the Node server was started with (`--port`, or
6
- the ROBLOX_STUDIO_MCP_PORT environment variable). It is stored as a plugin
7
- setting so a user running on a non-default port only has to set it once.
8
- ]]
9
-
10
- local Config = {}
11
-
12
- Config.PLUGIN_VERSION = "0.8.3"
13
-
14
- -- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
15
- -- computes the same hash from its own copy of the sources and compares, so a
16
- -- plugin left running from an older build is reported rather than silently
17
- -- answering with stale handlers. Stays "dev" when the tree is loaded unbuilt.
18
- Config.BUILD_ID = "dev"
19
- Config.PROTOCOL_VERSION = 1
20
- Config.DEFAULT_PORT = 44755
21
-
22
- -- Header the bridge requires on every request. A web page cannot set it
23
- -- cross-origin without a preflight the bridge never answers, which is what
24
- -- keeps a malicious site from driving Studio through the loopback port.
25
- Config.CLIENT_HEADER = "x-roblox-studio-mcp"
26
-
27
- -- How long to wait before retrying after the stream drops. Studio closes SSE
28
- -- connections at the 30 minute mark, so reconnects are routine, not exceptional.
29
- Config.RECONNECT_DELAY = 1
30
- Config.MAX_RECONNECT_DELAY = 30
31
-
32
- -- How long a connection has to last before it counts as having worked, rather
33
- -- than as one more failure to back off from. Comfortably longer than a failed
34
- -- handshake against a dead port, which resolves in milliseconds, and far shorter
35
- -- than any connection that carried a single command.
36
- Config.HEALTHY_CONNECTION = 10
37
-
38
- -- How long a healthy long-poll session runs before standing aside so streaming
39
- -- can be tried again. Long enough that a Studio which genuinely cannot stream
40
- -- is not reconnecting constantly, short enough that a session downgraded by one
41
- -- unlucky attempt does not spend the rest of its life slower than it should be.
42
- Config.RESTREAM_INTERVAL = 60
43
-
44
- local port: number? = nil
45
-
46
- function Config.setPort(value: number)
47
- port = value
48
- end
49
-
50
- function Config.getPort(): number
51
- return port or Config.DEFAULT_PORT
52
- end
53
-
54
- function Config.baseUrl(): string
55
- return string.format("http://127.0.0.1:%d", Config.getPort())
56
- end
57
-
58
- function Config.headers(): { [string]: string }
59
- return {
60
- [Config.CLIENT_HEADER] = tostring(Config.PROTOCOL_VERSION),
61
- ["Content-Type"] = "application/json",
62
- }
63
- end
64
-
65
- return Config
1
+ --!strict
2
+ --[[
3
+ Connection settings for the Studio MCP plugin.
4
+
5
+ The port must match the one the Node server was started with (`--port`, or
6
+ the ROBLOX_STUDIO_MCP_PORT environment variable). It is stored as a plugin
7
+ setting so a user running on a non-default port only has to set it once.
8
+ ]]
9
+
10
+ local Config = {}
11
+
12
+ Config.PLUGIN_VERSION = "0.8.4"
13
+
14
+ -- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
15
+ -- computes the same hash from its own copy of the sources and compares, so a
16
+ -- plugin left running from an older build is reported rather than silently
17
+ -- answering with stale handlers. Stays "dev" when the tree is loaded unbuilt.
18
+ Config.BUILD_ID = "dev"
19
+ Config.PROTOCOL_VERSION = 1
20
+ Config.DEFAULT_PORT = 44755
21
+
22
+ -- Header the bridge requires on every request. A web page cannot set it
23
+ -- cross-origin without a preflight the bridge never answers, which is what
24
+ -- keeps a malicious site from driving Studio through the loopback port.
25
+ Config.CLIENT_HEADER = "x-roblox-studio-mcp"
26
+
27
+ -- How long to wait before retrying after the stream drops. Studio closes SSE
28
+ -- connections at the 30 minute mark, so reconnects are routine, not exceptional.
29
+ Config.RECONNECT_DELAY = 1
30
+ Config.MAX_RECONNECT_DELAY = 30
31
+
32
+ -- How long a connection has to last before it counts as having worked, rather
33
+ -- than as one more failure to back off from. Comfortably longer than a failed
34
+ -- handshake against a dead port, which resolves in milliseconds, and far shorter
35
+ -- than any connection that carried a single command.
36
+ Config.HEALTHY_CONNECTION = 10
37
+
38
+ -- How long a healthy long-poll session runs before standing aside so streaming
39
+ -- can be tried again. Long enough that a Studio which genuinely cannot stream
40
+ -- is not reconnecting constantly, short enough that a session downgraded by one
41
+ -- unlucky attempt does not spend the rest of its life slower than it should be.
42
+ Config.RESTREAM_INTERVAL = 60
43
+
44
+ local port: number? = nil
45
+
46
+ function Config.setPort(value: number)
47
+ port = value
48
+ end
49
+
50
+ function Config.getPort(): number
51
+ return port or Config.DEFAULT_PORT
52
+ end
53
+
54
+ function Config.baseUrl(): string
55
+ return string.format("http://127.0.0.1:%d", Config.getPort())
56
+ end
57
+
58
+ function Config.headers(): { [string]: string }
59
+ return {
60
+ [Config.CLIENT_HEADER] = tostring(Config.PROTOCOL_VERSION),
61
+ ["Content-Type"] = "application/json",
62
+ }
63
+ end
64
+
65
+ return Config
@@ -1870,6 +1870,11 @@ function Console.setPromptBusy(busy: boolean)
1870
1870
  Visuals.setThinking(busy)
1871
1871
  end
1872
1872
 
1873
+ -- Whether the prompt row invites sentences. See `chat` in Commands.
1874
+ function Console.setPromptChat(enabled: boolean)
1875
+ Prompt.setChat(enabled)
1876
+ end
1877
+
1873
1878
  --[[
1874
1879
  Switches the whole panel to the active preset.
1875
1880
 
@@ -162,6 +162,10 @@ type Runtime = {
162
162
  -- back returns it rather than an empty line.
163
163
  draft: string,
164
164
  busy: boolean,
165
+ -- Whether a line that is not a command goes to an agent. Off until the user
166
+ -- runs `chat on`; only used to word the placeholder, since `Commands` is what
167
+ -- actually refuses the line.
168
+ chat: boolean,
165
169
  --[[
166
170
  What is selected in Studio right now, already worded for display.
167
171
 
@@ -195,6 +199,7 @@ local runtime: Runtime = {
195
199
  offset = 0,
196
200
  draft = "",
197
201
  busy = false,
202
+ chat = false,
198
203
  selection = "",
199
204
  complete = nil,
200
205
  suggest = nil,
@@ -218,9 +223,12 @@ local function paintCaret()
218
223
  end
219
224
 
220
225
  local function placeholder(): string
221
- return if runtime.busy
222
- then "running -- type stop to cancel"
223
- else "type a command, or ask for something"
226
+ if runtime.busy then
227
+ return "running -- type stop to cancel"
228
+ end
229
+ return if runtime.chat
230
+ then "type a command, or ask for something"
231
+ else "type a command -- `chat on` lets an agent answer"
224
232
  end
225
233
 
226
234
  --[[
@@ -903,6 +911,18 @@ function Prompt.setBusy(busy: boolean)
903
911
  end
904
912
  end
905
913
 
914
+ --[[
915
+ Reports whether sentences are answered, so the placeholder does not invite
916
+ a request the console is about to decline.
917
+ ]]
918
+ function Prompt.setChat(enabled: boolean)
919
+ runtime.chat = enabled
920
+ local field = runtime.field
921
+ if field ~= nil then
922
+ field.PlaceholderText = placeholder()
923
+ end
924
+ end
925
+
906
926
  --[[
907
927
  Reports what Studio has selected, as a phrase rather than a list.
908
928
 
@@ -65,6 +65,9 @@ local SETTING_WIDGET_OPEN = "widgetOpen"
65
65
  ]]
66
66
  local SETTING_AUTO_OPEN = "autoOpen"
67
67
  local SETTING_PLAYTESTS = "playtestsEnabled"
68
+ -- Whether sentences typed at the prompt are sent to an agent. Absent means off:
69
+ -- it only turns on when the user runs `chat on`.
70
+ local SETTING_CHAT = "chatEnabled"
68
71
  local SETTING_WIDGET_SIZE = "widgetSize"
69
72
  local SETTING_WIDGET_TRACE = "widgetTrace"
70
73
 
@@ -261,6 +264,8 @@ local wantOpen = if typeof(storedOpen) == "boolean" then storedOpen else true
261
264
  left exactly as it was, so turning this back on restores the panel to however
262
265
  it was last left rather than to a default.
263
266
  ]]
267
+ local chat = plugin:GetSetting(SETTING_CHAT) == true
268
+
264
269
  local storedAutoOpen = plugin:GetSetting(SETTING_AUTO_OPEN)
265
270
  local autoOpen = if typeof(storedAutoOpen) == "boolean" then storedAutoOpen else true
266
271
  if not autoOpen then
@@ -560,12 +565,21 @@ Commands.setup({
560
565
  autoOpen = value
561
566
  plugin:SetSetting(SETTING_AUTO_OPEN, value)
562
567
  end,
568
+ chat = function()
569
+ return chat
570
+ end,
571
+ saveChat = function(value)
572
+ chat = value
573
+ plugin:SetSetting(SETTING_CHAT, value)
574
+ end,
563
575
  status = function()
564
576
  return currentStatus
565
577
  end,
566
578
  studioId = studioId,
567
579
  })
568
580
 
581
+ Console.setPromptChat(chat)
582
+
569
583
  -- The saved preset, now that there is something to paint. Done immediately
570
584
  -- after mounting and before the first line is logged, so nothing is ever drawn
571
585
  -- in the wrong palette.
@@ -30,6 +30,7 @@ if (luau === null) {
30
30
  const suites = [
31
31
  { module: "plugin/src/handlers/Playtest.luau", test: "tests/playtests.luau", prelude: "tests/playtests-stub.luau" },
32
32
  { module: "plugin/src/Commands.luau", test: "tests/playtests-commands.luau", prelude: "tests/playtests-stub.luau", dependency: "plugin/src/handlers/Playtest.luau" },
33
+ { module: "plugin/src/Commands.luau", test: "tests/chat-commands.luau", prelude: "tests/playtests-stub.luau", dependency: "plugin/src/handlers/Playtest.luau" },
33
34
  { module: "plugin/src/RemoteTrace.luau", test: "tests/remote-trace.luau", prelude: "tests/remote-trace-stub.luau" },
34
35
  { module: "plugin/src/ExecRuntime.luau", test: "tests/exec-runtime.luau", prelude: "tests/exec-runtime-stub.luau" },
35
36
  { module: "plugin/src/ClientRelay.luau", test: "tests/client-relay.luau", prelude: "tests/client-relay-stub.luau" },