@el4cteo/rbx-studio-mcp 0.3.9 → 0.4.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/dist/bridge/failover.js +140 -0
- package/dist/bridge/failover.js.map +1 -0
- package/dist/bridge/remote.js +2 -1
- package/dist/bridge/remote.js.map +1 -1
- package/dist/bridge/server.js +93 -61
- package/dist/bridge/server.js.map +1 -1
- package/package.json +2 -2
- package/plugin/src/Config.luau +7 -1
- package/plugin/src/Transport.luau +22 -5
- package/plugin/src/Visuals.luau +1281 -1142
- package/scripts/test-failover.mjs +95 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks that the bridge port outlives the process that happened to bind it.
|
|
3
|
+
*
|
|
4
|
+
* Exactly one process can hold the port the Studio plugin dials; every other
|
|
5
|
+
* MCP client that starts this server proxies through it. The bug this covers is
|
|
6
|
+
* what used to happen when the holder exited: the peers kept posting to a socket
|
|
7
|
+
* nobody was listening on, so every tool call failed and the plugin's console
|
|
8
|
+
* sat on "Nothing is listening on port 44755" -- with a healthy server process
|
|
9
|
+
* still running that could have taken over. Reported by a user whose agent said
|
|
10
|
+
* it was connected while the console showed the opposite.
|
|
11
|
+
*
|
|
12
|
+
* Runs against real sockets on a spare loopback port, because the thing being
|
|
13
|
+
* tested is the bind race itself and a fake would test the fake.
|
|
14
|
+
*
|
|
15
|
+
* Usage: node scripts/test-failover.mjs
|
|
16
|
+
*/
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { createServer } from "node:http";
|
|
19
|
+
import { startBridgeServer } from "../dist/bridge/server.js";
|
|
20
|
+
import { CLIENT_HEADER } from "../dist/lib/protocol.js";
|
|
21
|
+
|
|
22
|
+
const PORT = 44799;
|
|
23
|
+
|
|
24
|
+
/** Waits for `check` to hold, or gives up loudly rather than hanging the suite. */
|
|
25
|
+
async function until(check, what, timeoutMs = 20_000) {
|
|
26
|
+
const deadline = Date.now() + timeoutMs;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
if (await check()) return;
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
30
|
+
}
|
|
31
|
+
assert.fail(`timed out waiting for ${what}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handshake(port, studioId) {
|
|
35
|
+
return fetch(`http://127.0.0.1:${port}/connect`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { [CLIENT_HEADER]: "test", "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
studioId,
|
|
40
|
+
placeName: "Test place",
|
|
41
|
+
placeId: 1,
|
|
42
|
+
pluginVersion: "test",
|
|
43
|
+
buildId: "test",
|
|
44
|
+
protocolVersion: 1,
|
|
45
|
+
transport: "poll",
|
|
46
|
+
context: "edit",
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A peer takes the port over when the owner exits, and really serves on it.
|
|
52
|
+
{
|
|
53
|
+
const owner = await startBridgeServer({ port: PORT });
|
|
54
|
+
assert.equal(owner.owner, true, "the first instance owns the port");
|
|
55
|
+
|
|
56
|
+
const peer = await startBridgeServer({ port: PORT });
|
|
57
|
+
assert.equal(peer.owner, false, "the second instance proxies");
|
|
58
|
+
|
|
59
|
+
// Both see the same Studio, because there is only one bridge.
|
|
60
|
+
await handshake(PORT, "studio-a");
|
|
61
|
+
assert.equal((await peer.bridge.sessions()).list.length, 1, "the peer sees the owner's session");
|
|
62
|
+
|
|
63
|
+
await owner.close();
|
|
64
|
+
await until(() => peer.owner, "the peer to take the port over");
|
|
65
|
+
|
|
66
|
+
// Owning the port is not the claim; serving on it is. The plugin reconnects
|
|
67
|
+
// after a handover, so a fresh handshake has to land on the new owner.
|
|
68
|
+
const response = await handshake(PORT, "studio-b");
|
|
69
|
+
assert.equal(response.status, 200, "the new owner answers the plugin");
|
|
70
|
+
const sessions = await peer.bridge.sessions();
|
|
71
|
+
assert.deepEqual(
|
|
72
|
+
sessions.list.map((session) => session.studioId),
|
|
73
|
+
["studio-b"],
|
|
74
|
+
"the new owner serves its own bridge",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
await peer.bridge.goodbye();
|
|
78
|
+
await peer.close();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A stranger on the port is still refused rather than proxied to.
|
|
82
|
+
{
|
|
83
|
+
const stranger = createServer((_req, res) => {
|
|
84
|
+
res.writeHead(404).end();
|
|
85
|
+
});
|
|
86
|
+
await new Promise((resolve) => stranger.listen(PORT, "127.0.0.1", resolve));
|
|
87
|
+
await assert.rejects(
|
|
88
|
+
() => startBridgeServer({ port: PORT }),
|
|
89
|
+
/is in use by something that is not roblox-studio-mcp/,
|
|
90
|
+
"posting Luau at an unknown server is never the right move",
|
|
91
|
+
);
|
|
92
|
+
await new Promise((resolve) => stranger.close(resolve));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
process.stdout.write("failover: ok\n");
|