@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.
@@ -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.6.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.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
@@ -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