@el4cteo/rbx-studio-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/bridge/rpc.js +243 -0
- package/dist/bridge/rpc.js.map +1 -0
- package/dist/bridge/server.js +281 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/apidump.js +269 -0
- package/dist/lib/apidump.js.map +1 -0
- package/dist/lib/errors.js +38 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.js +191 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/pluginbuild.js +83 -0
- package/dist/lib/pluginbuild.js.map +1 -0
- package/dist/lib/png.js +84 -0
- package/dist/lib/png.js.map +1 -0
- package/dist/lib/protocol.js +22 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/tool.js +27 -0
- package/dist/lib/tool.js.map +1 -0
- package/dist/resources.js +70 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools/api.js +78 -0
- package/dist/tools/api.js.map +1 -0
- package/dist/tools/character.js +94 -0
- package/dist/tools/character.js.map +1 -0
- package/dist/tools/debug.js +211 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/device.js +74 -0
- package/dist/tools/device.js.map +1 -0
- package/dist/tools/discover.js +217 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/exec.js +191 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/input.js +96 -0
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/instances.js +261 -0
- package/dist/tools/instances.js.map +1 -0
- package/dist/tools/perf.js +367 -0
- package/dist/tools/perf.js.map +1 -0
- package/dist/tools/playtest.js +153 -0
- package/dist/tools/playtest.js.map +1 -0
- package/dist/tools/screenshot.js +75 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/scripts.js +316 -0
- package/dist/tools/scripts.js.map +1 -0
- package/dist/tools/session.js +152 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/world.js +281 -0
- package/dist/tools/world.js.map +1 -0
- package/package.json +62 -0
- package/plugin/default.project.json +6 -0
- package/plugin/src/Config.luau +59 -0
- package/plugin/src/Console.luau +657 -0
- package/plugin/src/Context.luau +35 -0
- package/plugin/src/Dispatch.luau +90 -0
- package/plugin/src/Editor.luau +142 -0
- package/plugin/src/Emulation.luau +151 -0
- package/plugin/src/LogBuffer.luau +277 -0
- package/plugin/src/Net.luau +102 -0
- package/plugin/src/Paths.luau +255 -0
- package/plugin/src/Phrase.luau +465 -0
- package/plugin/src/Png.luau +238 -0
- package/plugin/src/Scope.luau +78 -0
- package/plugin/src/ScriptEdit.luau +100 -0
- package/plugin/src/Serialize.luau +287 -0
- package/plugin/src/TextEdit.luau +296 -0
- package/plugin/src/Transport.luau +328 -0
- package/plugin/src/Undo.luau +72 -0
- package/plugin/src/Visuals.luau +710 -0
- package/plugin/src/handlers/Api.luau +242 -0
- package/plugin/src/handlers/Assets.luau +145 -0
- package/plugin/src/handlers/Capture.luau +187 -0
- package/plugin/src/handlers/Character.luau +361 -0
- package/plugin/src/handlers/Debug.luau +391 -0
- package/plugin/src/handlers/Device.luau +119 -0
- package/plugin/src/handlers/Discover.luau +289 -0
- package/plugin/src/handlers/Exec.luau +270 -0
- package/plugin/src/handlers/Geometry.luau +261 -0
- package/plugin/src/handlers/Input.luau +287 -0
- package/plugin/src/handlers/Instances.luau +389 -0
- package/plugin/src/handlers/Perf.luau +645 -0
- package/plugin/src/handlers/Playtest.luau +205 -0
- package/plugin/src/handlers/Scripts.luau +387 -0
- package/plugin/src/handlers/Session.luau +168 -0
- package/plugin/src/handlers/Viewport.luau +302 -0
- package/plugin/src/handlers/World.luau +176 -0
- package/plugin/src/init.server.luau +317 -0
- package/scripts/build-plugin.mjs +157 -0
- package/scripts/check-plugin.mjs +97 -0
- package/scripts/install-plugin.mjs +39 -0
- package/scripts/latency.mjs +201 -0
- package/scripts/locate-luau.mjs +51 -0
- package/scripts/sourcemap.mjs +58 -0
- package/scripts/test-plugin.mjs +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Roblox Studio MCP
|
|
2
|
+
|
|
3
|
+
MCP server for Roblox Studio. 29 tools over a push-based bridge. MIT.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
**1. The Studio plugin**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @el4cteo/rbx-studio-mcp --install-plugin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or download `StudioMCP.rbxmx` from [Releases](https://github.com/EL4CTEO/roblox-studio-mcp/releases) into your Studio plugins folder.
|
|
14
|
+
|
|
15
|
+
**2. The server**, in whichever client you use:
|
|
16
|
+
|
|
17
|
+
<details>
|
|
18
|
+
<summary><b>Claude Code</b></summary>
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
claude mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp
|
|
22
|
+
```
|
|
23
|
+
</details>
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary><b>Codex CLI</b></summary>
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
codex mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or in `~/.codex/config.toml`:
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[mcp_servers.roblox-studio]
|
|
36
|
+
command = "npx"
|
|
37
|
+
args = ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
38
|
+
```
|
|
39
|
+
</details>
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary><b>Cursor</b></summary>
|
|
43
|
+
|
|
44
|
+
`~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"roblox-studio": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
</details>
|
|
57
|
+
|
|
58
|
+
<details>
|
|
59
|
+
<summary><b>opencode</b></summary>
|
|
60
|
+
|
|
61
|
+
`opencode.json`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"$schema": "https://opencode.ai/config.json",
|
|
66
|
+
"mcp": {
|
|
67
|
+
"roblox-studio": {
|
|
68
|
+
"type": "local",
|
|
69
|
+
"command": ["npx", "-y", "@el4cteo/rbx-studio-mcp"],
|
|
70
|
+
"enabled": true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
</details>
|
|
76
|
+
|
|
77
|
+
<details>
|
|
78
|
+
<summary><b>Claude Desktop</b></summary>
|
|
79
|
+
|
|
80
|
+
`claude_desktop_config.json`:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"roblox-studio": {
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
</details>
|
|
93
|
+
|
|
94
|
+
<details>
|
|
95
|
+
<summary><b>VS Code / Copilot</b></summary>
|
|
96
|
+
|
|
97
|
+
`.vscode/mcp.json`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"servers": {
|
|
102
|
+
"roblox-studio": {
|
|
103
|
+
"type": "stdio",
|
|
104
|
+
"command": "npx",
|
|
105
|
+
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
<details>
|
|
113
|
+
<summary><b>Gemini CLI</b></summary>
|
|
114
|
+
|
|
115
|
+
`~/.gemini/settings.json`:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"roblox-studio": {
|
|
121
|
+
"command": "npx",
|
|
122
|
+
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
</details>
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary><b>Windsurf</b></summary>
|
|
131
|
+
|
|
132
|
+
`~/.codeium/windsurf/mcp_config.json`:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"mcpServers": {
|
|
137
|
+
"roblox-studio": {
|
|
138
|
+
"command": "npx",
|
|
139
|
+
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
</details>
|
|
145
|
+
|
|
146
|
+
**3.** Open Studio, accept the `127.0.0.1` prompt, check the **Studio MCP** toolbar button. Verify with `studio_status`.
|
|
147
|
+
|
|
148
|
+
Port defaults to **44755** — `--port` or `ROBLOX_STUDIO_MCP_PORT`, matched in the plugin widget. Loopback only.
|
|
149
|
+
|
|
150
|
+
## Tools
|
|
151
|
+
|
|
152
|
+
| | |
|
|
153
|
+
|---|---|
|
|
154
|
+
| **Discover** | `studio_status` `tree` `inspect` `find` `api` |
|
|
155
|
+
| **Scripts** | `script_read` `script_edit` `script_grep` `script_create` |
|
|
156
|
+
| **Instances** | `create` `modify` `delete` `move` |
|
|
157
|
+
| **World** | `geometry` `assets` `collision` `undo` |
|
|
158
|
+
| **Run & debug** | `playtest` `execute_luau` `character` `input` `console` `debug` `performance` |
|
|
159
|
+
| **Look** | `screenshot` `viewport` `device` |
|
|
160
|
+
| **Session** | `list_studios` `set_active_studio` |
|
|
161
|
+
|
|
162
|
+
Gotchas: `screenshot` doesn't work mid-playtest. `debug` needs **API debugger Luau** in `File → Beta Features`. `device` emulation persists until `device op="stop"`.
|
|
163
|
+
|
|
164
|
+
## Compared to what else exists
|
|
165
|
+
|
|
166
|
+
| | tools | transport | editor-safe writes | undo recording | live API dump | licence |
|
|
167
|
+
|---|---|---|---|---|---|---|
|
|
168
|
+
| **this** | 29 | **SSE push** | **yes** | **yes** | **yes** | MIT |
|
|
169
|
+
| [Roblox built-in](https://create.roblox.com/docs/studio/mcp) | ~27 | stdio | partial | — | n/a | closed source |
|
|
170
|
+
| [Chrrxs](https://github.com/Chrrxs/robloxstudio-mcp) | ~40 | poll | no | partial | no | MIT |
|
|
171
|
+
| [drgost1](https://github.com/drgost1/robloxstudio-mcp) | 51 | poll 500 ms | no | yes | no | MIT |
|
|
172
|
+
| [boshyxd](https://github.com/boshyxd/robloxstudio-mcp) | 43 | long-poll | no | no | no | MIT (archived) |
|
|
173
|
+
| [Roblox/studio-rust-mcp-server](https://github.com/Roblox/studio-rust-mcp-server) | 2 | HTTP | no | no | no | MIT (superseded) |
|
|
174
|
+
|
|
175
|
+
- **Push, not poll.** SSE stream instead of a 500 ms poll. 50 sequential round trips: **13.6 ms** mean vs 25.8 ms, **12.8 ms** median vs 29.9 ms. Reproduce with `node scripts/latency.mjs --count 50 --compare`.
|
|
176
|
+
- **Safe script edits.** `ScriptEditorService:UpdateSourceAsync`, not `script.Source` — your unsaved editor buffer survives. Batches are all-or-nothing.
|
|
177
|
+
- **One Ctrl+Z per action**, via `ChangeHistoryService`.
|
|
178
|
+
- **29 tools, ~16k tokens of schema**, against 43–51 elsewhere. Cursor-paged, capped, with `detail: concise | standard | full`.
|
|
179
|
+
- **Live API dump** for property validation, so typos get suggestions (`Anchorred` → `Anchored`).
|
|
180
|
+
|
|
181
|
+
Not built here: terrain, AI mesh and material generation.
|
|
182
|
+
|
|
183
|
+
## Security
|
|
184
|
+
|
|
185
|
+
Binds `127.0.0.1`, rejects requests carrying `Origin`, and requires a header a browser cannot set cross-origin — closing the DNS-rebinding hole. Studio grants HTTP per plugin and per URL, so your experience's "Allow HTTP Requests" setting is untouched.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npm install
|
|
191
|
+
npm run build # TypeScript -> dist/
|
|
192
|
+
npm run build:plugin # plugin/src -> build/StudioMCP.rbxmx
|
|
193
|
+
npm run install:plugin # build + copy into the Studio plugins folder
|
|
194
|
+
npm test # Luau unit tests
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Needs `luau`, `luau-compile` and `luau-analyze` from [the Luau releases](https://github.com/luau-lang/luau/releases) on `PATH` or in `tools/`.
|
|
198
|
+
|
|
199
|
+
`evals/` holds ten questions answerable only by driving a real Studio session — see [evals/README.md](evals/README.md).
|
|
200
|
+
|
|
201
|
+
## Licence
|
|
202
|
+
|
|
203
|
+
MIT.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AMBIGUOUS_STUDIO, DISCONNECTED, NO_STUDIO, TIMEOUT, ToolError, } from "../lib/errors.js";
|
|
3
|
+
/** Default per-command deadline. Studio round trips are single-digit ms over SSE. */
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
5
|
+
/** How long a long-poll request is parked before we answer "idle". */
|
|
6
|
+
export const POLL_HOLD_MS = 25_000;
|
|
7
|
+
/** A session is considered dead if we have not heard from it in this long. */
|
|
8
|
+
const STALE_AFTER_MS = 90_000;
|
|
9
|
+
/**
|
|
10
|
+
* Owns every connected Studio and the in-flight request table.
|
|
11
|
+
*
|
|
12
|
+
* The two transports converge here: `deliver` either writes straight to an open
|
|
13
|
+
* SSE stream or parks the command for the next long-poll. Tools call `call()`
|
|
14
|
+
* and never learn which one was used.
|
|
15
|
+
*/
|
|
16
|
+
export class Bridge {
|
|
17
|
+
sessions = new Map();
|
|
18
|
+
activeStudioId = null;
|
|
19
|
+
/**
|
|
20
|
+
* Whether `activeStudioId` was picked by the user via set_active_studio, as
|
|
21
|
+
* opposed to being the only session that happened to connect first.
|
|
22
|
+
*
|
|
23
|
+
* The distinction decides what happens when a second Studio appears. A
|
|
24
|
+
* defaulted target silently becomes ambiguous — editing whichever place
|
|
25
|
+
* connected first is not a reasonable guess — while a chosen one stays put,
|
|
26
|
+
* because the user said which place they meant.
|
|
27
|
+
*/
|
|
28
|
+
activeWasChosen = false;
|
|
29
|
+
// --- session lifecycle -------------------------------------------------
|
|
30
|
+
attach(identity, stream) {
|
|
31
|
+
const existing = this.sessions.get(identity.studioId);
|
|
32
|
+
if (existing) {
|
|
33
|
+
// Studio reconnected (SSE hit the 30-minute cap, or the plugin reloaded).
|
|
34
|
+
// Keep pending calls alive across the gap so an in-flight tool survives.
|
|
35
|
+
existing.stream?.end();
|
|
36
|
+
existing.identity = identity;
|
|
37
|
+
existing.stream = stream;
|
|
38
|
+
existing.lastSeenAt = Date.now();
|
|
39
|
+
this.flush(existing);
|
|
40
|
+
return identity.studioId;
|
|
41
|
+
}
|
|
42
|
+
this.sessions.set(identity.studioId, {
|
|
43
|
+
identity,
|
|
44
|
+
connectedAt: Date.now(),
|
|
45
|
+
lastSeenAt: Date.now(),
|
|
46
|
+
stream,
|
|
47
|
+
queue: [],
|
|
48
|
+
waiter: null,
|
|
49
|
+
pending: new Map(),
|
|
50
|
+
});
|
|
51
|
+
this.activeStudioId ??= identity.studioId;
|
|
52
|
+
return identity.studioId;
|
|
53
|
+
}
|
|
54
|
+
detach(studioId) {
|
|
55
|
+
const session = this.sessions.get(studioId);
|
|
56
|
+
if (!session)
|
|
57
|
+
return;
|
|
58
|
+
for (const pending of session.pending.values()) {
|
|
59
|
+
clearTimeout(pending.timer);
|
|
60
|
+
pending.reject(DISCONNECTED());
|
|
61
|
+
}
|
|
62
|
+
session.pending.clear();
|
|
63
|
+
session.waiter?.(null);
|
|
64
|
+
session.stream?.end();
|
|
65
|
+
this.sessions.delete(studioId);
|
|
66
|
+
if (this.activeStudioId === studioId) {
|
|
67
|
+
// The place the user chose is gone, so the replacement is a fallback, not
|
|
68
|
+
// a choice: mark it as such so a remaining pair goes ambiguous again.
|
|
69
|
+
this.activeStudioId = this.sessions.keys().next().value ?? null;
|
|
70
|
+
this.activeWasChosen = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Drops sessions whose plugin stopped checking in without a clean detach. */
|
|
74
|
+
reapStale() {
|
|
75
|
+
const cutoff = Date.now() - STALE_AFTER_MS;
|
|
76
|
+
for (const [id, session] of this.sessions) {
|
|
77
|
+
if (session.lastSeenAt < cutoff && !session.stream)
|
|
78
|
+
this.detach(id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
touch(studioId) {
|
|
82
|
+
const session = this.sessions.get(studioId);
|
|
83
|
+
if (session)
|
|
84
|
+
session.lastSeenAt = Date.now();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Records the place name a status call resolved.
|
|
88
|
+
*
|
|
89
|
+
* The identity announced at connect can only carry the data model's name
|
|
90
|
+
* ("Place1"), because the real one takes a web lookup. Once any call has paid
|
|
91
|
+
* for that lookup, every later mention of the session should use the name the
|
|
92
|
+
* user recognises rather than reverting to the one they do not.
|
|
93
|
+
*/
|
|
94
|
+
notePlaceName(studioId, placeName, context) {
|
|
95
|
+
const session = this.sessions.get(studioId);
|
|
96
|
+
if (!session)
|
|
97
|
+
return;
|
|
98
|
+
if (placeName)
|
|
99
|
+
session.identity.placeName = placeName;
|
|
100
|
+
if (context)
|
|
101
|
+
session.identity.context = context;
|
|
102
|
+
}
|
|
103
|
+
has(studioId) {
|
|
104
|
+
return this.sessions.has(studioId);
|
|
105
|
+
}
|
|
106
|
+
list() {
|
|
107
|
+
return [...this.sessions.values()].map((session) => ({
|
|
108
|
+
...session.identity,
|
|
109
|
+
connectedAt: session.connectedAt,
|
|
110
|
+
lastSeenAt: session.lastSeenAt,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
get activeId() {
|
|
114
|
+
return this.activeStudioId;
|
|
115
|
+
}
|
|
116
|
+
/** True only once the user has actually picked a target. */
|
|
117
|
+
get activeIsChosen() {
|
|
118
|
+
return this.activeWasChosen;
|
|
119
|
+
}
|
|
120
|
+
setActive(studioId) {
|
|
121
|
+
if (!this.sessions.has(studioId)) {
|
|
122
|
+
throw new ToolError("UNKNOWN_STUDIO", `No connected Studio has id "${studioId}".`, "Call list_studios to see the connected instances and their ids.");
|
|
123
|
+
}
|
|
124
|
+
this.activeStudioId = studioId;
|
|
125
|
+
this.activeWasChosen = true;
|
|
126
|
+
}
|
|
127
|
+
// --- request/response --------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Sends `op` to a Studio instance and resolves with its `data` payload.
|
|
130
|
+
* Rejects with a ToolError carrying an agent-readable hint on any failure,
|
|
131
|
+
* including errors raised inside the plugin.
|
|
132
|
+
*/
|
|
133
|
+
call(op, params = {}, options = {}) {
|
|
134
|
+
const session = this.resolveSession(options.studioId);
|
|
135
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
136
|
+
const command = { id: randomUUID(), op, params };
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
session.pending.delete(command.id);
|
|
140
|
+
reject(TIMEOUT(op, timeoutMs));
|
|
141
|
+
}, timeoutMs);
|
|
142
|
+
session.pending.set(command.id, {
|
|
143
|
+
resolve: resolve,
|
|
144
|
+
reject,
|
|
145
|
+
timer,
|
|
146
|
+
op,
|
|
147
|
+
});
|
|
148
|
+
this.deliver(session, command);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Settles the promise a plugin result belongs to. Unknown ids are ignored. */
|
|
152
|
+
settle(studioId, result) {
|
|
153
|
+
const session = this.sessions.get(studioId);
|
|
154
|
+
if (!session)
|
|
155
|
+
return;
|
|
156
|
+
session.lastSeenAt = Date.now();
|
|
157
|
+
const pending = session.pending.get(result.id);
|
|
158
|
+
if (!pending)
|
|
159
|
+
return; // already timed out; the tool has moved on
|
|
160
|
+
session.pending.delete(result.id);
|
|
161
|
+
clearTimeout(pending.timer);
|
|
162
|
+
if (result.ok) {
|
|
163
|
+
pending.resolve(result.data);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
pending.reject(result.error
|
|
167
|
+
? ToolError.fromCommandError(result.error)
|
|
168
|
+
: new ToolError("PLUGIN_ERROR", `Studio failed to run "${pending.op}".`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Parks a long-poll request until a command arrives or the hold expires.
|
|
173
|
+
* Resolves null on expiry so the plugin can re-poll with a fresh request
|
|
174
|
+
* rather than sitting on a connection Studio may time out underneath it.
|
|
175
|
+
*/
|
|
176
|
+
waitForCommand(studioId) {
|
|
177
|
+
const session = this.sessions.get(studioId);
|
|
178
|
+
if (!session)
|
|
179
|
+
return Promise.resolve(null);
|
|
180
|
+
session.lastSeenAt = Date.now();
|
|
181
|
+
const queued = session.queue.shift();
|
|
182
|
+
if (queued)
|
|
183
|
+
return Promise.resolve(queued);
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
const settle = (command) => {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
session.waiter = null;
|
|
188
|
+
resolve(command);
|
|
189
|
+
};
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
if (session.waiter === settle)
|
|
192
|
+
session.waiter = null;
|
|
193
|
+
resolve(null);
|
|
194
|
+
}, POLL_HOLD_MS);
|
|
195
|
+
session.waiter = settle;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// --- internals ---------------------------------------------------------
|
|
199
|
+
resolveSession(studioId) {
|
|
200
|
+
if (studioId) {
|
|
201
|
+
const session = this.sessions.get(studioId);
|
|
202
|
+
if (!session)
|
|
203
|
+
throw NO_STUDIO();
|
|
204
|
+
return session;
|
|
205
|
+
}
|
|
206
|
+
if (this.sessions.size === 0)
|
|
207
|
+
throw NO_STUDIO();
|
|
208
|
+
// One Studio is never ambiguous, whether or not anyone chose it.
|
|
209
|
+
const only = this.sessions.values().next().value;
|
|
210
|
+
if (this.sessions.size === 1 && only)
|
|
211
|
+
return only;
|
|
212
|
+
if (this.activeWasChosen && this.activeStudioId) {
|
|
213
|
+
const session = this.sessions.get(this.activeStudioId);
|
|
214
|
+
if (session)
|
|
215
|
+
return session;
|
|
216
|
+
}
|
|
217
|
+
throw AMBIGUOUS_STUDIO(
|
|
218
|
+
// The context is the part that decides it. Two rows reading "Untitled
|
|
219
|
+
// Experience" are indistinguishable, and picking the playtest means work
|
|
220
|
+
// that disappears when it stops.
|
|
221
|
+
this.list().map((session) => `${session.studioId} (${session.placeName}${session.context ? `, ${session.context}` : ""})`));
|
|
222
|
+
}
|
|
223
|
+
deliver(session, command) {
|
|
224
|
+
if (session.stream && !session.stream.writableEnded) {
|
|
225
|
+
session.stream.write(`data: ${JSON.stringify(command)}\n\n`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (session.waiter) {
|
|
229
|
+
session.waiter(command);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
session.queue.push(command);
|
|
233
|
+
}
|
|
234
|
+
/** Hands any queued commands to a stream that just (re)connected. */
|
|
235
|
+
flush(session) {
|
|
236
|
+
if (!session.stream || session.stream.writableEnded)
|
|
237
|
+
return;
|
|
238
|
+
for (const command of session.queue.splice(0)) {
|
|
239
|
+
session.stream.write(`data: ${JSON.stringify(command)}\n\n`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=rpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../../src/bridge/rpc.ts"],"names":[],"mappings":"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"}
|