@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 +4 -3
- package/package.json +1 -1
- package/plugin/src/Commands.luau +72 -5
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +5 -0
- package/plugin/src/Prompt.luau +23 -3
- package/plugin/src/init.server.luau +14 -0
- package/scripts/test-plugin.mjs +1 -0
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
|
|
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
|
-
**
|
|
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
package/plugin/src/Commands.luau
CHANGED
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
package/plugin/src/Config.luau
CHANGED
|
@@ -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.
|
|
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
|
package/plugin/src/Console.luau
CHANGED
|
@@ -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
|
|
package/plugin/src/Prompt.luau
CHANGED
|
@@ -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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
package/scripts/test-plugin.mjs
CHANGED
|
@@ -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" },
|