@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.
Files changed (68) hide show
  1. package/README.md +24 -6
  2. package/dist/bridge/rpc.js +1 -1
  3. package/dist/bridge/rpc.js.map +1 -1
  4. package/dist/index.js +7 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/apidump.js +75 -0
  7. package/dist/lib/apidump.js.map +1 -1
  8. package/dist/lib/errors.js +5 -4
  9. package/dist/lib/errors.js.map +1 -1
  10. package/dist/lib/format.js +92 -25
  11. package/dist/lib/format.js.map +1 -1
  12. package/dist/lib/notices.js +29 -0
  13. package/dist/lib/notices.js.map +1 -0
  14. package/dist/lib/protocol.js.map +1 -1
  15. package/dist/lib/sync.js +1141 -0
  16. package/dist/lib/sync.js.map +1 -0
  17. package/dist/lib/syncplan.js +338 -0
  18. package/dist/lib/syncplan.js.map +1 -0
  19. package/dist/lib/tool.js +9 -2
  20. package/dist/lib/tool.js.map +1 -1
  21. package/dist/tools/discover.js +6 -1
  22. package/dist/tools/discover.js.map +1 -1
  23. package/dist/tools/exec.js +5 -0
  24. package/dist/tools/exec.js.map +1 -1
  25. package/dist/tools/instances.js +2 -2
  26. package/dist/tools/instances.js.map +1 -1
  27. package/dist/tools/perf.js +28 -3
  28. package/dist/tools/perf.js.map +1 -1
  29. package/dist/tools/screenshot.js +7 -3
  30. package/dist/tools/screenshot.js.map +1 -1
  31. package/dist/tools/scripts.js +110 -28
  32. package/dist/tools/scripts.js.map +1 -1
  33. package/dist/tools/sync.js +169 -0
  34. package/dist/tools/sync.js.map +1 -0
  35. package/package.json +4 -4
  36. package/plugin/src/Commands.luau +72 -5
  37. package/plugin/src/Config.luau +65 -65
  38. package/plugin/src/Console.luau +5 -0
  39. package/plugin/src/Dispatch.luau +131 -90
  40. package/plugin/src/ExecRuntime.luau +190 -168
  41. package/plugin/src/LogBuffer.luau +38 -9
  42. package/plugin/src/Paths.luau +42 -0
  43. package/plugin/src/Phrase.luau +41 -0
  44. package/plugin/src/Prompt.luau +23 -3
  45. package/plugin/src/ScriptEdit.luau +94 -8
  46. package/plugin/src/Serialize.luau +16 -1
  47. package/plugin/src/Transport.luau +5 -2
  48. package/plugin/src/Undo.luau +74 -10
  49. package/plugin/src/handlers/Capture.luau +818 -809
  50. package/plugin/src/handlers/Debug.luau +19 -16
  51. package/plugin/src/handlers/Discover.luau +31 -1
  52. package/plugin/src/handlers/Perf.luau +100 -21
  53. package/plugin/src/handlers/Scripts.luau +274 -90
  54. package/plugin/src/handlers/Sync.luau +968 -0
  55. package/plugin/src/init.server.luau +47 -2
  56. package/scripts/build.mjs +14 -0
  57. package/scripts/sync-fake.mjs +191 -0
  58. package/scripts/test-live-sync-scale.mjs +150 -0
  59. package/scripts/test-live-sync.mjs +232 -0
  60. package/scripts/test-live-tools.mjs +6 -1
  61. package/scripts/test-plugin.mjs +17 -0
  62. package/scripts/test-results.mjs +41 -0
  63. package/scripts/test-sync-more.mjs +228 -0
  64. package/scripts/test-sync.mjs +245 -0
  65. package/dist/tools/spatial.js +0 -135
  66. package/dist/tools/spatial.js.map +0 -1
  67. package/dist/tools/upload.js +0 -294
  68. package/dist/tools/upload.js.map +0 -1
@@ -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.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
@@ -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
 
@@ -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
- function Dispatch.register(op: string, handler: Handler)
23
- handlers[op] = handler
24
- end
25
-
26
- --[[
27
- Registers every entry of `map` under the prefix, e.g. `registerAll("script",
28
- { read = fn })` exposes "script.read".
29
- ]]
30
- function Dispatch.registerAll(prefix: string, map: { [string]: Handler })
31
- for name, handler in map do
32
- handlers[prefix .. "." .. name] = handler
33
- end
34
- end
35
-
36
- --[[
37
- Raises a structured failure. Always call this rather than `error("...")` so
38
- the agent receives a code it can branch on and a hint it can act on.
39
- ]]
40
- function Dispatch.fail(code: string, message: string, hint: string?): never
41
- error({ code = code, message = message, hint = hint }, 0)
42
- end
43
-
44
- export type Result = {
45
- id: string,
46
- ok: boolean,
47
- data: any?,
48
- error: Error?,
49
- }
50
-
51
- --[[
52
- Runs one command and returns the frame to POST back. Never throws: an
53
- unhandled error inside a handler becomes a HANDLER_ERROR result, because a
54
- crashed dispatch would leave the server's promise hanging until it times out.
55
- ]]
56
- function Dispatch.invoke(id: string, op: string, params: { [string]: any }?): Result
57
- local handler = handlers[op]
58
- if not handler then
59
- return {
60
- id = id,
61
- ok = false,
62
- error = {
63
- code = "UNKNOWN_OP",
64
- message = string.format('This Studio plugin has no handler for "%s".', op),
65
- hint = "The plugin is older than the MCP server. Update the Studio MCP "
66
- .. "plugin to the version matching your server.",
67
- },
68
- }
69
- end
70
-
71
- local ok, result = pcall(handler, params or {})
72
- if ok then
73
- return { id = id, ok = true, data = result }
74
- end
75
-
76
- if typeof(result) == "table" and (result :: any).code then
77
- return { id = id, ok = false, error = result :: Error }
78
- end
79
-
80
- return {
81
- id = id,
82
- ok = false,
83
- error = {
84
- code = "HANDLER_ERROR",
85
- message = string.format('"%s" failed: %s', op, tostring(result)),
86
- },
87
- }
88
- end
89
-
90
- return Dispatch
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