@el4cteo/rbx-studio-mcp 0.5.3 → 0.5.5
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 +37 -151
- package/dist/bridge/remote.js +17 -3
- package/dist/bridge/remote.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/scripts/test-failover.mjs +51 -0
package/README.md
CHANGED
|
@@ -1,54 +1,35 @@
|
|
|
1
1
|
# Roblox Studio MCP
|
|
2
2
|
|
|
3
|
-
Let an AI agent drive Roblox Studio: read your place, edit scripts, build
|
|
3
|
+
Let an AI agent drive Roblox Studio: read your place, edit scripts, build geometry, run playtests, take screenshots. 31 tools. MIT.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
**1. The
|
|
9
|
+
**1. The plugin**
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx -y @el4cteo/rbx-studio-mcp --install-plugin
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Or
|
|
15
|
+
Or drop `StudioMCP.rbxmx` from [Releases](https://github.com/EL4CTEO/rbx-studio-mcp/releases) into your Studio plugins folder.
|
|
16
16
|
|
|
17
|
-
**2. The server
|
|
18
|
-
|
|
19
|
-
<details>
|
|
20
|
-
<summary><b>Claude Code</b></summary>
|
|
17
|
+
**2. The server**
|
|
21
18
|
|
|
22
19
|
```bash
|
|
23
20
|
claude mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp
|
|
24
21
|
```
|
|
25
|
-
</details>
|
|
26
22
|
|
|
27
23
|
<details>
|
|
28
|
-
<summary
|
|
24
|
+
<summary>Other clients</summary>
|
|
25
|
+
|
|
26
|
+
Codex CLI:
|
|
29
27
|
|
|
30
28
|
```bash
|
|
31
29
|
codex mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp
|
|
32
30
|
```
|
|
33
|
-
</details>
|
|
34
|
-
|
|
35
|
-
<details>
|
|
36
|
-
<summary><b>Cursor</b> — <code>~/.cursor/mcp.json</code> or <code>.cursor/mcp.json</code></summary>
|
|
37
|
-
|
|
38
|
-
```json
|
|
39
|
-
{
|
|
40
|
-
"mcpServers": {
|
|
41
|
-
"roblox-studio": {
|
|
42
|
-
"command": "npx",
|
|
43
|
-
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
</details>
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
<summary><b>Claude Desktop</b> — <code>claude_desktop_config.json</code></summary>
|
|
32
|
+
Cursor, Claude Desktop, Gemini CLI, Windsurf — add to their config file:
|
|
52
33
|
|
|
53
34
|
```json
|
|
54
35
|
{
|
|
@@ -60,60 +41,13 @@ codex mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp
|
|
|
60
41
|
}
|
|
61
42
|
}
|
|
62
43
|
```
|
|
63
|
-
</details>
|
|
64
|
-
|
|
65
|
-
<details>
|
|
66
|
-
<summary><b>Gemini CLI</b> — <code>~/.gemini/settings.json</code></summary>
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
"mcpServers": {
|
|
71
|
-
"roblox-studio": {
|
|
72
|
-
"command": "npx",
|
|
73
|
-
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
</details>
|
|
45
|
+
VS Code / Copilot (`.vscode/mcp.json`) uses `"servers"` instead of `"mcpServers"`, plus `"type": "stdio"`.
|
|
79
46
|
|
|
80
|
-
|
|
81
|
-
<summary><b>Windsurf</b> — <code>~/.codeium/windsurf/mcp_config.json</code></summary>
|
|
47
|
+
opencode (`opencode.json`):
|
|
82
48
|
|
|
83
49
|
```json
|
|
84
50
|
{
|
|
85
|
-
"mcpServers": {
|
|
86
|
-
"roblox-studio": {
|
|
87
|
-
"command": "npx",
|
|
88
|
-
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
</details>
|
|
94
|
-
|
|
95
|
-
<details>
|
|
96
|
-
<summary><b>VS Code / Copilot</b> — <code>.vscode/mcp.json</code></summary>
|
|
97
|
-
|
|
98
|
-
```json
|
|
99
|
-
{
|
|
100
|
-
"servers": {
|
|
101
|
-
"roblox-studio": {
|
|
102
|
-
"type": "stdio",
|
|
103
|
-
"command": "npx",
|
|
104
|
-
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
</details>
|
|
110
|
-
|
|
111
|
-
<details>
|
|
112
|
-
<summary><b>opencode</b> — <code>opencode.json</code></summary>
|
|
113
|
-
|
|
114
|
-
```json
|
|
115
|
-
{
|
|
116
|
-
"$schema": "https://opencode.ai/config.json",
|
|
117
51
|
"mcp": {
|
|
118
52
|
"roblox-studio": {
|
|
119
53
|
"type": "local",
|
|
@@ -125,25 +59,11 @@ codex mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp
|
|
|
125
59
|
```
|
|
126
60
|
</details>
|
|
127
61
|
|
|
128
|
-
**3.** Open Studio and accept the `127.0.0.1` prompt
|
|
129
|
-
|
|
130
|
-
**4.** `debug` additionally needs **Debugger Luau API** in File → Beta Features, plus a Studio restart. Nothing else requires it.
|
|
131
|
-
|
|
132
|
-

|
|
62
|
+
**3.** Open Studio and accept the `127.0.0.1` prompt. Check it works with `studio_status`.
|
|
133
63
|
|
|
134
|
-
|
|
64
|
+
Something wrong? Run `npx -y @el4cteo/rbx-studio-mcp doctor` — it says what is broken and how to fix it.
|
|
135
65
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
npx -y @el4cteo/rbx-studio-mcp doctor
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Multiple agents
|
|
143
|
-
|
|
144
|
-
Register the server in as many clients as you like. No extra configuration.
|
|
145
|
-
|
|
146
|
-
Each agent keeps its own target, so two agents can work on two open places. Subagents share their parent's target — give them an explicit `studioId` per call.
|
|
66
|
+
Port is **44755**, loopback only. Change it with `--port` and match it in the plugin.
|
|
147
67
|
|
|
148
68
|
## Tools
|
|
149
69
|
|
|
@@ -153,101 +73,67 @@ Each agent keeps its own target, so two agents can work on two open places. Suba
|
|
|
153
73
|
| **Discover** | `tree` `inspect` `find` `api` |
|
|
154
74
|
| **Scripts** | `script_read` `script_edit` `script_grep` `script_create` |
|
|
155
75
|
| **Instances** | `create` `modify` `delete` `move` |
|
|
156
|
-
| **World** | `geometry` `generate` `assets` `collision` `undo` |
|
|
76
|
+
| **World** | `geometry` `terrain` `generate` `assets` `collision` `undo` |
|
|
157
77
|
| **Run & debug** | `playtest` `execute_luau` `character` `input` `console` `debug` `performance` |
|
|
158
78
|
| **Look** | `screenshot` `viewport` `device` |
|
|
159
79
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
`generate` builds 3D models from a prompt using Roblox's Cube model — `Body1` for props, `Car5` for a body and four named wheels a script can drive, or your own part names.
|
|
80
|
+
Write tools take arrays — ten script edits is one call, one **Ctrl+Z**, and all-or-nothing.
|
|
163
81
|
|
|
164
|
-
|
|
82
|
+
Two things to watch: a playtest connects a second session, so pass `studioId` and use the edit one for changes that must last; `device` emulation stays on until `device op="stop"`.
|
|
165
83
|
|
|
166
84
|
## The console panel
|
|
167
85
|
|
|
168
|
-
Every call is logged with
|
|
169
|
-
|
|
170
|
-
Under the header is a command line. Type a command, or type a sentence and it goes to a coding agent.
|
|
86
|
+
Every call is logged with how long it took. Below the header is a command line — type a command, or type a sentence and a coding agent answers it.
|
|
171
87
|
|
|
172
88
|
| | |
|
|
173
89
|
|---|---|
|
|
174
90
|
| `help` | list everything |
|
|
175
|
-
| `doctor` | check
|
|
91
|
+
| `doctor` | check the setup |
|
|
176
92
|
| `status` `version` `place` `clients` | what this session is |
|
|
177
|
-
| `studios` `use <n>` | which Studio window calls
|
|
178
|
-
| `theme [name]` `visuals` `log [level]` `clear` `copy` | the panel
|
|
93
|
+
| `studios` `use <n>` | which Studio window calls go to |
|
|
94
|
+
| `theme [name]` `visuals` `log [level]` `clear` `copy` | the panel |
|
|
179
95
|
| `port [n]` `reconnect` | the connection |
|
|
180
|
-
| `agent [use <id>\|new]` `stop` | which
|
|
96
|
+
| `agent [use <id>\|new]` `stop` | which agent runs your prompts |
|
|
181
97
|
| anything else | sent to that agent |
|
|
182
98
|
|
|
183
|
-
|
|
99
|
+
Arrows walk the history, Tab completes.
|
|
184
100
|
|
|
185
|
-
**Prompts start a real agent
|
|
101
|
+
**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.
|
|
186
102
|
|
|
187
|
-
|
|
103
|
+
Eight themes behind the tab on the right edge. Your pick is remembered.
|
|
104
|
+
|
|
105
|
+
## Why this one
|
|
106
|
+
|
|
107
|
+
- **Push, not poll** — 13.6 ms per call against 25.8 ms.
|
|
108
|
+
- **Safe script edits** — writes go through the script editor, so unsaved work survives.
|
|
109
|
+
- **Stale edits are refused** — pass back the `rev` from `script_read` and a write lands only if nobody else touched the file.
|
|
110
|
+
- **Property names are checked** against the running engine, so `Anchorred` comes back as a suggestion, not a runtime error.
|
|
188
111
|
|
|
189
112
|
## DeepSeek Harness (dsh)
|
|
190
113
|
|
|
191
|
-
|
|
114
|
+
This server registers as a dsh plugin. `config/dsh.cordis.yml` is the row:
|
|
192
115
|
|
|
193
116
|
```sh
|
|
194
117
|
dsh --profile headless --patch config/dsh.cordis.yml "add a spawn point"
|
|
195
118
|
```
|
|
196
119
|
|
|
197
|
-
To keep it, append that row to your own
|
|
198
|
-
|
|
199
|
-
`config/dsh.cordis.yml` in this repo is the row. The console panel also lists `dsh` as an agent, and passes the overlay itself, so a prompt typed there works before you have merged anything.
|
|
200
|
-
|
|
201
|
-
Two limits, both dsh's rather than ours: its headless profile prints the final answer instead of streaming, so the panel shows the reply at the end rather than step by step, and it has no `--resume`, so every prompt is a fresh conversation. It needs `DEEPSEEK_API_KEY`.
|
|
202
|
-
|
|
203
|
-
## Batching
|
|
204
|
-
|
|
205
|
-
Every write tool takes an array — ten script edits is one call.
|
|
206
|
-
|
|
207
|
-
| tool | takes | cap |
|
|
208
|
-
|---|---|---|
|
|
209
|
-
| `create` | instances, each nesting `children` to any depth | 100 |
|
|
210
|
-
| `modify` | entries, each with an unlimited list of `paths` | 100 entries |
|
|
211
|
-
| `delete` | paths | 200 |
|
|
212
|
-
| `move` | moves | 200 |
|
|
213
|
-
| `script_edit` | edits, across any number of scripts | 50 |
|
|
214
|
-
| `script_create` | scripts | 50 |
|
|
215
|
-
| `inspect` | paths | 50 |
|
|
216
|
-
| `input` | input steps, delivered in order | 40 |
|
|
217
|
-
|
|
218
|
-
`modify` caps *entries*, not targets — one entry can anchor five hundred parts.
|
|
219
|
-
|
|
220
|
-
Each batch is one **Ctrl+Z**, and all-or-nothing: a failed match leaves the place untouched.
|
|
221
|
-
|
|
222
|
-
## Why this one
|
|
223
|
-
|
|
224
|
-
- **Push, not poll.** 50 sequential round trips average 13.6 ms, against 25.8 ms polling — reproduce with `node scripts/latency.mjs --count 50 --compare`.
|
|
225
|
-
- **Safe script edits.** Writes go through `ScriptEditorService:UpdateSourceAsync`, so your unsaved editor buffer survives.
|
|
226
|
-
- **Edits can refuse to be stale.** `script_read` prints a `rev`; pass it back and the write is rejected if anyone changed the script meanwhile, instead of landing on lines that moved.
|
|
227
|
-
- **One Ctrl+Z per call.** Every batch is a single undo recording.
|
|
228
|
-
- **Property names are checked** against the running engine's API dump, so a typo comes back as `Anchorred` → `Anchored` instead of a runtime error.
|
|
229
|
-
- **~16k tokens of schema**, cursor-paged and capped, with `detail: concise | standard | full`.
|
|
230
|
-
|
|
231
|
-
Not built here: material generation.
|
|
120
|
+
To keep it, append that row to your own `cordis.patch.yml`. Needs `DEEPSEEK_API_KEY`.
|
|
232
121
|
|
|
233
122
|
## Security
|
|
234
123
|
|
|
235
|
-
Loopback only
|
|
124
|
+
Loopback only, and requires a header a browser cannot set cross-origin. Your experience's "Allow HTTP Requests" setting is untouched.
|
|
236
125
|
|
|
237
126
|
## Development
|
|
238
127
|
|
|
239
128
|
```bash
|
|
240
129
|
npm install
|
|
241
130
|
npm run build # TypeScript -> dist/
|
|
242
|
-
npm run
|
|
243
|
-
npm
|
|
244
|
-
npm test # plugin (Luau) + bridge (Node) tests
|
|
131
|
+
npm run install:plugin # build the plugin and copy it into Studio
|
|
132
|
+
npm test
|
|
245
133
|
```
|
|
246
134
|
|
|
247
135
|
Needs `luau`, `luau-compile` and `luau-analyze` from [the Luau releases](https://github.com/luau-lang/luau/releases) on `PATH` or in `tools/`.
|
|
248
136
|
|
|
249
|
-
`evals/` holds ten questions answerable only by driving a real Studio session — see [evals/README.md](evals/README.md).
|
|
250
|
-
|
|
251
137
|
## Licence
|
|
252
138
|
|
|
253
139
|
MIT.
|
package/dist/bridge/remote.js
CHANGED
|
@@ -121,9 +121,23 @@ export class RemoteBridge {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
async post(path, body, timeoutMs) {
|
|
124
|
-
|
|
124
|
+
//[[ The body read is inside the try, and that is the whole point of it.
|
|
125
|
+
//
|
|
126
|
+
// Only the fetch used to be guarded. But the owner does not always die
|
|
127
|
+
// before answering -- it dies while answering, which is exactly what a
|
|
128
|
+
// handover looks like: headers arrive, the process exits, and the socket
|
|
129
|
+
// closes mid-body. `response.json()` then throws its own raw
|
|
130
|
+
// "TypeError: fetch failed / SocketError: other side closed", outside the
|
|
131
|
+
// catch, and the user is handed a Node network error where OWNER_GONE
|
|
132
|
+
// belongs -- with none of the "try again in a few seconds" that makes it
|
|
133
|
+
// actionable.
|
|
134
|
+
//
|
|
135
|
+
// Reported as an error appearing right below the takeover notice, which is
|
|
136
|
+
// the one moment this is guaranteed to happen.
|
|
137
|
+
//]]
|
|
138
|
+
let payload;
|
|
125
139
|
try {
|
|
126
|
-
response = await fetch(`${this.base}${path}`, {
|
|
140
|
+
const response = await fetch(`${this.base}${path}`, {
|
|
127
141
|
method: "POST",
|
|
128
142
|
headers: this.headers,
|
|
129
143
|
body: JSON.stringify(body),
|
|
@@ -131,11 +145,11 @@ export class RemoteBridge {
|
|
|
131
145
|
// its diagnosis reaches the caller, rather than being cut off by ours.
|
|
132
146
|
signal: AbortSignal.timeout(timeoutMs + 10_000),
|
|
133
147
|
});
|
|
148
|
+
payload = (await response.json());
|
|
134
149
|
}
|
|
135
150
|
catch (cause) {
|
|
136
151
|
throw this.unreachable(cause);
|
|
137
152
|
}
|
|
138
|
-
const payload = (await response.json());
|
|
139
153
|
if (!payload.ok) {
|
|
140
154
|
const error = payload.error ?? { code: "PEER_ERROR", message: "the bridge owner refused" };
|
|
141
155
|
throw new ToolError(error.code, error.message);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote.js","sourceRoot":"","sources":["../../src/bridge/remote.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"remote.js","sourceRoot":"","sources":["../../src/bridge/remote.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAU5C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,0BAA0B,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,YAAY,GAAG,MAAM,CAAC;AAE5B;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC3C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,oBAAoB,IAAI,WAAW,EAAE;YAChE,OAAO,EAAE,EAAE,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE;YACpC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SACnC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAC9B,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAC;QAC/D,IAAI,IAAI,CAAC,MAAM,KAAK,mBAAmB;YAAE,OAAO,IAAI,CAAC;QACrD,IAAI,IAAI,CAAC,eAAe,KAAK,gBAAgB;YAAE,OAAO,IAAI,CAAC;QAC3D,OAAO,IAAqB,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,YAAY;IAiBJ,IAAI;IACZ,KAAK;IAjBP,OAAO,GAAG,KAAK,CAAC;IACR,QAAQ,GAAG,UAAU,EAAE,CAAC;IACjC,SAAS,GAA0B,IAAI,CAAC;IAEhD;;;;;;;;OAQG;IACK,KAAK,GAA6B,IAAI,CAAC;IAE/C,YACmB,IAAY,EACpB,KAAoB;oBADZ,IAAI;qBACZ,KAAK;QAEd,uEAAuE;QACvE,yEAAyE;QACzE,yDAAyD;QACzD,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE,EAAE,YAAY,CAAC,CAAC;QACpE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAED,oEAAoE;IAC5D,KAAK,CAAC,KAAK;QACjB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CACb,QAAQ,EACR;gBACE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI;gBACtB,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO;gBAC5B,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,qEAAqE;gBACrE,oEAAoE;gBACpE,OAAO,EAAE,gBAAgB;aAC1B,EACD,KAAK,CACN,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,aAAa;QACf,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,KAAwB;QAC/B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC;QAAC,MAAM,CAAC;YACP,aAAa;QACf,CAAC;IACH,CAAC;IAED,IAAY,IAAI;QACd,OAAO,oBAAoB,IAAI,CAAC,IAAI,EAAE,CAAC;IACzC,CAAC;IAED,IAAY,OAAO;QACjB,OAAO;YACL,CAAC,aAAa,CAAC,EAAE,MAAM;YACvB,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,QAAQ;YAC5B,cAAc,EAAE,kBAAkB;SACnC,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,IAAI,CAAI,IAAY,EAAE,IAAa,EAAE,SAAiB;QAClE,wEAAwE;QACxE,EAAE;QACF,uEAAuE;QACvE,uEAAuE;QACvE,yEAAyE;QACzE,6DAA6D;QAC7D,0EAA0E;QAC1E,sEAAsE;QACtE,yEAAyE;QACzE,cAAc;QACd,EAAE;QACF,2EAA2E;QAC3E,+CAA+C;QAC/C,IAAI;QACJ,IAAI,OAA6E,CAAC;QAClF,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,EAAE,EAAE;gBAClD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,yEAAyE;gBACzE,uEAAuE;gBACvE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC;aAChD,CAAC,CAAC;YACH,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAmB,CAAC;QACtD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;YAChB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC;YAC3F,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,OAAO,CAAC,IAAS,CAAC;IAC3B,CAAC;IAEO,WAAW,CAAC,KAAc;QAChC,OAAO,IAAI,SAAS,CAClB,YAAY,EACZ,+CAA+C,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI;YACjF,sBAAsB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAChF,+EAA+E;YAC7E,4EAA4E;YAC5E,+BAA+B,CAClC,CAAC;IACJ,CAAC;IAED,IAAI,CACF,EAAU,EACV,MAAM,GAA4B,EAAE,EACpC,OAAO,GAA8C,EAAE;QAEvD,OAAO,IAAI,CAAC,IAAI,CAAI,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC,CAAC;IACxF,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,WAAW,EAAE;gBACpD,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;aACnC,CAAC,CAAC;YACH,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAiB,CAAC;QACjD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,QAAgB;QAC9B,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAAgB,EAAE,SAAiB,EAAE,OAAgB;QACvE,2EAA2E;QAC3E,2DAA2D;QAC3D,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;QAC1E,CAAC;QAAC,MAAM,CAAC;YACP,aAAa;QACf,CAAC;IACH,CAAC;CACF"}
|
package/package.json
CHANGED
package/plugin/src/Config.luau
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
local Config = {}
|
|
11
11
|
|
|
12
|
-
Config.PLUGIN_VERSION = "0.5.
|
|
12
|
+
Config.PLUGIN_VERSION = "0.5.5"
|
|
13
13
|
|
|
14
14
|
-- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
|
|
15
15
|
-- computes the same hash from its own copy of the sources and compares, so a
|
|
@@ -180,4 +180,55 @@ function handshake(port, studioId) {
|
|
|
180
180
|
await owner.close();
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// An owner that dies WHILE answering is still OWNER_GONE, not a raw Node error.
|
|
184
|
+
//
|
|
185
|
+
// The shipped bug: only the fetch was guarded, so a socket cut after the
|
|
186
|
+
// headers threw "TypeError: fetch failed / SocketError: other side closed"
|
|
187
|
+
// straight past the catch. It surfaced right under the takeover notice, which
|
|
188
|
+
// is the exact moment a handover cuts a live response.
|
|
189
|
+
//
|
|
190
|
+
// The fake owner declares a Content-Length it never delivers, then hangs up.
|
|
191
|
+
// That distinction is the whole test: a socket destroyed BEFORE the headers
|
|
192
|
+
// makes Node reject the fetch itself, which the old code already caught -- the
|
|
193
|
+
// bug only shows when the reply has started and stops halfway. Two earlier
|
|
194
|
+
// versions of this test cut the socket too early, passed with the bug present,
|
|
195
|
+
// and proved nothing.
|
|
196
|
+
{
|
|
197
|
+
const halfDead = createServer((req, res) => {
|
|
198
|
+
if (req.url === "/identity") {
|
|
199
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
200
|
+
res.end(JSON.stringify({ server: "roblox-studio-mcp", protocolVersion: 1, pid: 1 }));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
res.writeHead(200, { "Content-Type": "application/json", "Content-Length": "5000" });
|
|
204
|
+
res.write('{"ok":true,"data":"' + "x".repeat(100));
|
|
205
|
+
setTimeout(() => res.socket.destroy(), 150);
|
|
206
|
+
});
|
|
207
|
+
await new Promise((resolve) => halfDead.listen(PORT, "127.0.0.1", resolve));
|
|
208
|
+
|
|
209
|
+
const peer = await startBridgeServer({ port: PORT });
|
|
210
|
+
assert.equal(peer.owner, false, "the peer proxies to whatever holds the port");
|
|
211
|
+
|
|
212
|
+
let raised;
|
|
213
|
+
try {
|
|
214
|
+
await peer.bridge.call("studio.status", {});
|
|
215
|
+
} catch (cause) {
|
|
216
|
+
raised = cause;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
assert.ok(raised, "a truncated answer must not resolve as success");
|
|
220
|
+
assert.equal(
|
|
221
|
+
raised.code,
|
|
222
|
+
"OWNER_GONE",
|
|
223
|
+
`a body cut short is the owner going away, not a raw ${raised?.name}`,
|
|
224
|
+
);
|
|
225
|
+
assert.ok(
|
|
226
|
+
/try the call again/.test(raised.hint ?? ""),
|
|
227
|
+
"and says what to do about it, which a raw socket error never does",
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await peer.close();
|
|
231
|
+
await new Promise((resolve) => halfDead.close(resolve));
|
|
232
|
+
}
|
|
233
|
+
|
|
183
234
|
process.stdout.write("failover: ok\n");
|