@el4cteo/rbx-studio-mcp 0.6.8 → 0.7.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/README.md +2 -2
- package/dist/bridge/harness.js +39 -12
- package/dist/bridge/harness.js.map +1 -1
- package/dist/bridge/rpc.js +26 -6
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/bridge/server.js +12 -4
- package/dist/bridge/server.js.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/exec.js +5 -4
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/input.js +9 -7
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/scripts.js +4 -2
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +11 -3
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Serialize.luau +10 -0
- package/plugin/src/TextEdit.luau +6 -6
- package/plugin/src/handlers/Discover.luau +690 -685
- package/plugin/src/handlers/Exec.luau +5 -1
- package/plugin/src/handlers/Input.luau +43 -9
- package/plugin/src/handlers/Scripts.luau +677 -673
- package/plugin/src/handlers/Terrain.luau +371 -371
- package/plugin/src/init.server.luau +939 -883
- package/scripts/test-bridge.mjs +328 -267
- package/scripts/test-console.mjs +592 -560
- package/scripts/test-server.mjs +60 -0
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.
|
|
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.7.1"
|
|
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
|
|
@@ -60,6 +60,16 @@ function Serialize.value(value: any): any
|
|
|
60
60
|
return value
|
|
61
61
|
end
|
|
62
62
|
if kind == "number" then
|
|
63
|
+
-- JSON has no inf or nan, and HttpService:JSONEncode turns them into a
|
|
64
|
+
-- `{"m":null,"t":"numeric","v":"inf"}` object nobody asked for. Written as
|
|
65
|
+
-- text instead, in the same words `modify` accepts back.
|
|
66
|
+
if value ~= value then
|
|
67
|
+
return "nan"
|
|
68
|
+
elseif value == math.huge then
|
|
69
|
+
return "inf"
|
|
70
|
+
elseif value == -math.huge then
|
|
71
|
+
return "-inf"
|
|
72
|
+
end
|
|
63
73
|
-- Kept numeric so the agent can do arithmetic without parsing.
|
|
64
74
|
return math.round(value * 10 ^ PRECISION) / 10 ^ PRECISION
|
|
65
75
|
end
|
package/plugin/src/TextEdit.luau
CHANGED
|
@@ -98,7 +98,7 @@ function TextEdit.validate(edit: Edit, position: number)
|
|
|
98
98
|
if typeof(edit.path) ~= "string" or edit.path == "" then
|
|
99
99
|
Dispatch.fail(
|
|
100
100
|
"BAD_PARAMS",
|
|
101
|
-
string.format("edits[%d] has no `path`.", position),
|
|
101
|
+
string.format("edits[%d] has no `path`.", position - 1),
|
|
102
102
|
'Every edit needs the path of the script to change, e.g. "ServerScriptService.Main".'
|
|
103
103
|
)
|
|
104
104
|
end
|
|
@@ -117,7 +117,7 @@ function TextEdit.validate(edit: Edit, position: number)
|
|
|
117
117
|
if modes == 0 then
|
|
118
118
|
Dispatch.fail(
|
|
119
119
|
"BAD_PARAMS",
|
|
120
|
-
string.format("edits[%d] (%s) says what to edit but not how.", position, edit.path),
|
|
120
|
+
string.format("edits[%d] (%s) says what to edit but not how.", position - 1, edit.path),
|
|
121
121
|
"Give exactly one of: `find` + `replace`, `startLine` (+ `endLine`) + "
|
|
122
122
|
.. "`replacement`, or `source` to replace the whole script."
|
|
123
123
|
)
|
|
@@ -125,7 +125,7 @@ function TextEdit.validate(edit: Edit, position: number)
|
|
|
125
125
|
if modes > 1 then
|
|
126
126
|
Dispatch.fail(
|
|
127
127
|
"BAD_PARAMS",
|
|
128
|
-
string.format("edits[%d] (%s) combines more than one edit mode.", position, edit.path),
|
|
128
|
+
string.format("edits[%d] (%s) combines more than one edit mode.", position - 1, edit.path),
|
|
129
129
|
"Use `find`, `startLine` or `source` -- one per edit. Send several entries "
|
|
130
130
|
.. "in the array if you need several changes."
|
|
131
131
|
)
|
|
@@ -135,13 +135,13 @@ function TextEdit.validate(edit: Edit, position: number)
|
|
|
135
135
|
if edit.find == "" then
|
|
136
136
|
Dispatch.fail(
|
|
137
137
|
"BAD_PARAMS",
|
|
138
|
-
string.format("edits[%d] (%s) has an empty `find`.", position, edit.path)
|
|
138
|
+
string.format("edits[%d] (%s) has an empty `find`.", position - 1, edit.path)
|
|
139
139
|
)
|
|
140
140
|
end
|
|
141
141
|
if typeof(edit.replace) ~= "string" then
|
|
142
142
|
Dispatch.fail(
|
|
143
143
|
"BAD_PARAMS",
|
|
144
|
-
string.format("edits[%d] (%s) has `find` but no `replace`.", position, edit.path),
|
|
144
|
+
string.format("edits[%d] (%s) has `find` but no `replace`.", position - 1, edit.path),
|
|
145
145
|
"Pass an empty `replace` to delete the matched text."
|
|
146
146
|
)
|
|
147
147
|
end
|
|
@@ -149,7 +149,7 @@ function TextEdit.validate(edit: Edit, position: number)
|
|
|
149
149
|
if typeof(edit.replacement) ~= "string" then
|
|
150
150
|
Dispatch.fail(
|
|
151
151
|
"BAD_PARAMS",
|
|
152
|
-
string.format("edits[%d] (%s) has `startLine` but no `replacement`.", position, edit.path),
|
|
152
|
+
string.format("edits[%d] (%s) has `startLine` but no `replacement`.", position - 1, edit.path),
|
|
153
153
|
"Pass an empty `replacement` to delete those lines."
|
|
154
154
|
)
|
|
155
155
|
end
|