@el4cteo/rbx-studio-mcp 0.8.3 → 0.8.6
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 +24 -6
- package/dist/bridge/rpc.js +1 -1
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/apidump.js +75 -0
- package/dist/lib/apidump.js.map +1 -1
- package/dist/lib/errors.js +5 -4
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/format.js +92 -25
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/notices.js +29 -0
- package/dist/lib/notices.js.map +1 -0
- package/dist/lib/protocol.js.map +1 -1
- package/dist/lib/sync.js +1141 -0
- package/dist/lib/sync.js.map +1 -0
- package/dist/lib/syncplan.js +338 -0
- package/dist/lib/syncplan.js.map +1 -0
- package/dist/lib/tool.js +9 -2
- package/dist/lib/tool.js.map +1 -1
- package/dist/tools/discover.js +6 -1
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +5 -0
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/instances.js +2 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +28 -3
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/screenshot.js +7 -3
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/scripts.js +110 -28
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/sync.js +169 -0
- package/dist/tools/sync.js.map +1 -0
- package/package.json +4 -4
- package/plugin/src/Commands.luau +72 -5
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +5 -0
- package/plugin/src/Dispatch.luau +131 -90
- package/plugin/src/ExecRuntime.luau +190 -168
- package/plugin/src/LogBuffer.luau +38 -9
- package/plugin/src/Paths.luau +42 -0
- package/plugin/src/Phrase.luau +41 -0
- package/plugin/src/Prompt.luau +23 -3
- package/plugin/src/ScriptEdit.luau +94 -8
- package/plugin/src/Serialize.luau +16 -1
- package/plugin/src/Transport.luau +5 -2
- package/plugin/src/Undo.luau +74 -10
- package/plugin/src/handlers/Capture.luau +818 -809
- package/plugin/src/handlers/Debug.luau +19 -16
- package/plugin/src/handlers/Discover.luau +31 -1
- package/plugin/src/handlers/Perf.luau +100 -21
- package/plugin/src/handlers/Scripts.luau +274 -90
- package/plugin/src/handlers/Sync.luau +968 -0
- package/plugin/src/init.server.luau +47 -2
- package/scripts/build.mjs +14 -0
- package/scripts/sync-fake.mjs +191 -0
- package/scripts/test-live-sync-scale.mjs +150 -0
- package/scripts/test-live-sync.mjs +232 -0
- package/scripts/test-live-tools.mjs +6 -1
- package/scripts/test-plugin.mjs +17 -0
- package/scripts/test-results.mjs +41 -0
- package/scripts/test-sync-more.mjs +228 -0
- package/scripts/test-sync.mjs +245 -0
- package/dist/tools/spatial.js +0 -135
- package/dist/tools/spatial.js.map +0 -1
- package/dist/tools/upload.js +0 -294
- package/dist/tools/upload.js.map +0 -1
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.6"
|
|
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/Dispatch.luau
CHANGED
|
@@ -1,90 +1,131 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
Handler registry and the single error boundary around every command.
|
|
4
|
-
|
|
5
|
-
Handlers raise failures with `Dispatch.fail(code, message, hint)`. The hint is
|
|
6
|
-
forwarded verbatim to the agent, so it should say what to do next ("call find
|
|
7
|
-
first to get a valid path"), not just what went wrong.
|
|
8
|
-
]]
|
|
9
|
-
|
|
10
|
-
local Dispatch = {}
|
|
11
|
-
|
|
12
|
-
export type Error = {
|
|
13
|
-
code: string,
|
|
14
|
-
message: string,
|
|
15
|
-
hint: string?,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type Handler = (params: { [string]: any }) -> any
|
|
19
|
-
|
|
20
|
-
local handlers: { [string]: Handler } = {}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Handler registry and the single error boundary around every command.
|
|
4
|
+
|
|
5
|
+
Handlers raise failures with `Dispatch.fail(code, message, hint)`. The hint is
|
|
6
|
+
forwarded verbatim to the agent, so it should say what to do next ("call find
|
|
7
|
+
first to get a valid path"), not just what went wrong.
|
|
8
|
+
]]
|
|
9
|
+
|
|
10
|
+
local Dispatch = {}
|
|
11
|
+
|
|
12
|
+
export type Error = {
|
|
13
|
+
code: string,
|
|
14
|
+
message: string,
|
|
15
|
+
hint: string?,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Handler = (params: { [string]: any }) -> any
|
|
19
|
+
|
|
20
|
+
local handlers: { [string]: Handler } = {}
|
|
21
|
+
|
|
22
|
+
--[[
|
|
23
|
+
When the server stops waiting for each running command, keyed by the
|
|
24
|
+
command's thread.
|
|
25
|
+
|
|
26
|
+
The server drops a timed-out command it has not delivered yet, but one
|
|
27
|
+
already delivered can still be sitting behind the mutation lock, or behind
|
|
28
|
+
a slow editor write. Starting it then does work nobody will hear about --
|
|
29
|
+
and the agent, told "timed out", may be retrying it at that moment. So the
|
|
30
|
+
deadline travels with the command and is checked where work would begin.
|
|
31
|
+
|
|
32
|
+
Both clocks are this machine's, so there is no skew to allow for. Weak keys,
|
|
33
|
+
so a thread that dies mid-command does not leave its entry behind.
|
|
34
|
+
]]
|
|
35
|
+
local deadlines: { [thread]: number } = setmetatable({}, { __mode = "k" }) :: any
|
|
36
|
+
|
|
37
|
+
function Dispatch.deadline(): number?
|
|
38
|
+
return deadlines[coroutine.running()]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
--[[
|
|
42
|
+
Refuses to go on once the command's deadline has passed. Pass the deadline
|
|
43
|
+
explicitly from code that runs on another thread -- an engine callback, or
|
|
44
|
+
a spawned task -- which the table above cannot see.
|
|
45
|
+
]]
|
|
46
|
+
function Dispatch.checkDeadline(explicit: number?)
|
|
47
|
+
local deadline = explicit or Dispatch.deadline()
|
|
48
|
+
if deadline and DateTime.now().UnixTimestampMillis >= deadline then
|
|
49
|
+
Dispatch.fail(
|
|
50
|
+
"DEADLINE_EXPIRED",
|
|
51
|
+
"The request timed out before Studio started this step, so it was not done.",
|
|
52
|
+
"Earlier steps of the same request may have applied. Check the current state before retrying."
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
function Dispatch.register(op: string, handler: Handler)
|
|
58
|
+
handlers[op] = handler
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
--[[
|
|
62
|
+
Registers every entry of `map` under the prefix, e.g. `registerAll("script",
|
|
63
|
+
{ read = fn })` exposes "script.read".
|
|
64
|
+
]]
|
|
65
|
+
function Dispatch.registerAll(prefix: string, map: { [string]: Handler })
|
|
66
|
+
for name, handler in map do
|
|
67
|
+
handlers[prefix .. "." .. name] = handler
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
--[[
|
|
72
|
+
Raises a structured failure. Always call this rather than `error("...")` so
|
|
73
|
+
the agent receives a code it can branch on and a hint it can act on.
|
|
74
|
+
]]
|
|
75
|
+
function Dispatch.fail(code: string, message: string, hint: string?): never
|
|
76
|
+
error({ code = code, message = message, hint = hint }, 0)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
export type Result = {
|
|
80
|
+
id: string,
|
|
81
|
+
ok: boolean,
|
|
82
|
+
data: any?,
|
|
83
|
+
error: Error?,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
--[[
|
|
87
|
+
Runs one command and returns the frame to POST back. Never throws: an
|
|
88
|
+
unhandled error inside a handler becomes a HANDLER_ERROR result, because a
|
|
89
|
+
crashed dispatch would leave the server's promise hanging until it times out.
|
|
90
|
+
]]
|
|
91
|
+
function Dispatch.invoke(id: string, op: string, params: { [string]: any }?, deadlineMs: number?): Result
|
|
92
|
+
local handler = handlers[op]
|
|
93
|
+
if not handler then
|
|
94
|
+
return {
|
|
95
|
+
id = id,
|
|
96
|
+
ok = false,
|
|
97
|
+
error = {
|
|
98
|
+
code = "UNKNOWN_OP",
|
|
99
|
+
message = string.format('This Studio plugin has no handler for "%s".', op),
|
|
100
|
+
hint = "The plugin is older than the MCP server. Update the Studio MCP "
|
|
101
|
+
.. "plugin to the version matching your server.",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
local thread = coroutine.running()
|
|
107
|
+
deadlines[thread] = deadlineMs
|
|
108
|
+
local ok, result = pcall(function()
|
|
109
|
+
Dispatch.checkDeadline()
|
|
110
|
+
return handler(params or {})
|
|
111
|
+
end)
|
|
112
|
+
deadlines[thread] = nil
|
|
113
|
+
if ok then
|
|
114
|
+
return { id = id, ok = true, data = result }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if typeof(result) == "table" and (result :: any).code then
|
|
118
|
+
return { id = id, ok = false, error = result :: Error }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
id = id,
|
|
123
|
+
ok = false,
|
|
124
|
+
error = {
|
|
125
|
+
code = "HANDLER_ERROR",
|
|
126
|
+
message = string.format('"%s" failed: %s', op, tostring(result)),
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return Dispatch
|