@el4cteo/rbx-studio-mcp 0.5.4 → 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 +2 -0
- 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
|
@@ -12,6 +12,8 @@ Let an AI agent drive Roblox Studio: read your place, edit scripts, build geomet
|
|
|
12
12
|
npx -y @el4cteo/rbx-studio-mcp --install-plugin
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
Or drop `StudioMCP.rbxmx` from [Releases](https://github.com/EL4CTEO/rbx-studio-mcp/releases) into your Studio plugins folder.
|
|
16
|
+
|
|
15
17
|
**2. The server**
|
|
16
18
|
|
|
17
19
|
```bash
|
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":"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"}
|
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");
|