@el4cteo/rbx-studio-mcp 0.1.4 → 0.1.6
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 +155 -250
- package/dist/index.js +1 -1
- package/dist/lib/errors.js +5 -3
- package/dist/lib/errors.js.map +1 -1
- package/dist/tools/session.js +2 -7
- package/dist/tools/session.js.map +1 -1
- package/package.json +2 -2
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/handlers/Debug.luau +40 -10
- package/plugin/src/handlers/Session.luau +169 -185
package/README.md
CHANGED
|
@@ -1,250 +1,155 @@
|
|
|
1
|
-
# Roblox Studio MCP
|
|
2
|
-
|
|
3
|
-
MCP server for Roblox Studio
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
**1. The Studio plugin**
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npx -y @el4cteo/rbx-studio-mcp --install-plugin
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Or download `StudioMCP.rbxmx` from [Releases](https://github.com/EL4CTEO/rbx-studio-mcp/releases) into your Studio plugins folder.
|
|
16
|
-
|
|
17
|
-
**2. The server**, in whichever client you use:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
**Several agents at once work.** Register this in as many clients as you like — two Claude sessions, Claude plus Cursor, whatever. The plugin connects out to one port, so the first server to start owns it and the rest proxy through it automatically. Nothing to configure, and no second connection to Studio.
|
|
157
|
-
|
|
158
|
-
Each agent keeps its **own** target: `set_active_studio` binds per client, so two agents can work on two open places at once and neither can retarget the other. Pass `studioId` on a single call to reach elsewhere without changing your default.
|
|
159
|
-
|
|
160
|
-
Isolation is per MCP connection, and **subagents share their parent's** — measured, not assumed. A subagent calling `set_active_studio` retargets its parent and its siblings, silently: later calls still succeed, just against the wrong place. Give subagents an explicit `studioId` per call instead. Nothing on the wire distinguishes a subagent from its parent, so this is guidance rather than something the server can enforce.
|
|
161
|
-
|
|
162
|
-
## Tools
|
|
163
|
-
|
|
164
|
-
| | |
|
|
165
|
-
|---|---|
|
|
166
|
-
| **Discover** | `studio_status` `tree` `inspect` `find` `api` |
|
|
167
|
-
| **Scripts** | `script_read` `script_edit` `script_grep` `script_create` |
|
|
168
|
-
| **Instances** | `create` `modify` `delete` `move` |
|
|
169
|
-
| **World** | `geometry` `assets` `collision` `undo` |
|
|
170
|
-
| **Run & debug** | `playtest` `execute_luau` `character` `input` `console` `debug` `performance` |
|
|
171
|
-
| **Look** | `screenshot` `viewport` `device` |
|
|
172
|
-
| **Session** | `list_studios` `set_active_studio` |
|
|
173
|
-
|
|
174
|
-
Gotchas: `debug` needs **API debugger Luau** in `File → Beta Features`. `device` emulation persists until `device op="stop"`. During a playtest two sessions connect — pass `studioId` explicitly, using the edit session for anything that must persist.
|
|
175
|
-
|
|
176
|
-
## Batching
|
|
177
|
-
|
|
178
|
-
Every write tool takes an array. Editing ten scripts, deleting two hundred
|
|
179
|
-
parts or anchoring a whole map is **one call**, not a loop.
|
|
180
|
-
|
|
181
|
-
| tool | takes | cap |
|
|
182
|
-
|---|---|---|
|
|
183
|
-
| `create` | instances, each nesting `children` to any depth | 100 |
|
|
184
|
-
| `modify` | entries, each with an unlimited list of `paths` | 100 entries |
|
|
185
|
-
| `delete` | paths | 200 |
|
|
186
|
-
| `move` | moves | 200 |
|
|
187
|
-
| `script_edit` | edits, across any number of scripts | 50 |
|
|
188
|
-
| `script_create` | scripts | 50 |
|
|
189
|
-
| `inspect` | paths | 50 |
|
|
190
|
-
| `input` | input steps, delivered in order | 40 |
|
|
191
|
-
|
|
192
|
-
The cap on `modify` is on *entries*, not targets — one entry can anchor five
|
|
193
|
-
hundred parts, so pair it with `find` and change a whole place in a single call.
|
|
194
|
-
|
|
195
|
-
What that buys beyond speed:
|
|
196
|
-
|
|
197
|
-
- **One Ctrl+Z.** The batch runs inside a single `ChangeHistoryService`
|
|
198
|
-
recording named after the tool. Ten script edits undo as one step, not ten.
|
|
199
|
-
- **All-or-nothing.** Everything is read and transformed in memory before
|
|
200
|
-
anything is written, so a find that matches nothing — or matches twice —
|
|
201
|
-
fails with the place untouched instead of half-edited.
|
|
202
|
-
- **Nesting.** `create` takes `children`, so a whole model arrives in one call.
|
|
203
|
-
A new instance's path is not knowable until it exists, and same-named siblings
|
|
204
|
-
make guessing it unreliable.
|
|
205
|
-
|
|
206
|
-
Parallel tool calls work too — responses are keyed by request id, so several
|
|
207
|
-
calls in one turn run concurrently. Prefer a batch where one exists: parallel
|
|
208
|
-
calls are N round trips and N undo steps, a batch is one of each.
|
|
209
|
-
|
|
210
|
-
## Compared to what else exists
|
|
211
|
-
|
|
212
|
-
| | tools | transport | editor-safe writes | undo recording | live API dump | licence |
|
|
213
|
-
|---|---|---|---|---|---|---|
|
|
214
|
-
| **this** | 29 | **SSE push** | **yes** | **yes** | **yes** | MIT |
|
|
215
|
-
| [Roblox built-in](https://create.roblox.com/docs/studio/mcp) | ~27 | stdio | partial | — | n/a | closed source |
|
|
216
|
-
| [Chrrxs](https://github.com/Chrrxs/robloxstudio-mcp) | ~40 | poll | no | partial | no | MIT |
|
|
217
|
-
| [drgost1](https://github.com/drgost1/robloxstudio-mcp) | 51 | poll 500 ms | no | yes | no | MIT |
|
|
218
|
-
| [boshyxd](https://github.com/boshyxd/robloxstudio-mcp) | 43 | long-poll | no | no | no | MIT (archived) |
|
|
219
|
-
| [Roblox/studio-rust-mcp-server](https://github.com/Roblox/studio-rust-mcp-server) | 2 | HTTP | no | no | no | MIT (superseded) |
|
|
220
|
-
|
|
221
|
-
- **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`.
|
|
222
|
-
- **Safe script edits.** `ScriptEditorService:UpdateSourceAsync`, not `script.Source` — your unsaved editor buffer survives.
|
|
223
|
-
- **One Ctrl+Z per action**, via `ChangeHistoryService`.
|
|
224
|
-
- **29 tools, ~16k tokens of schema**, against 43–51 elsewhere. Cursor-paged, capped, with `detail: concise | standard | full`.
|
|
225
|
-
- **Live API dump** for property validation, so typos get suggestions (`Anchorred` → `Anchored`).
|
|
226
|
-
- **Several agents, one Studio**, each with its own target — no id needed on every call, and no agent able to retarget another. The built-in server reaches the same isolation by making `studio_id` mandatory everywhere and removing `set_active_studio`; this keeps the default, so a single open Studio needs no id at all.
|
|
227
|
-
|
|
228
|
-
Not built here: terrain, AI mesh and material generation.
|
|
229
|
-
|
|
230
|
-
## Security
|
|
231
|
-
|
|
232
|
-
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.
|
|
233
|
-
|
|
234
|
-
## Development
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
npm install
|
|
238
|
-
npm run build # TypeScript -> dist/
|
|
239
|
-
npm run build:plugin # plugin/src -> build/StudioMCP.rbxmx
|
|
240
|
-
npm run install:plugin # build + copy into the Studio plugins folder
|
|
241
|
-
npm test # Luau unit tests
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Needs `luau`, `luau-compile` and `luau-analyze` from [the Luau releases](https://github.com/luau-lang/luau/releases) on `PATH` or in `tools/`.
|
|
245
|
-
|
|
246
|
-
`evals/` holds ten questions answerable only by driving a real Studio session — see [evals/README.md](evals/README.md).
|
|
247
|
-
|
|
248
|
-
## Licence
|
|
249
|
-
|
|
250
|
-
MIT.
|
|
1
|
+
# Roblox Studio MCP
|
|
2
|
+
|
|
3
|
+
MCP server for Roblox Studio: 29 tools over a push-based bridge, batched writes that undo as one step, editor-safe script edits. MIT.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
**1. The Studio plugin**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y @el4cteo/rbx-studio-mcp --install-plugin
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or download `StudioMCP.rbxmx` from [Releases](https://github.com/EL4CTEO/rbx-studio-mcp/releases) into your Studio plugins folder.
|
|
16
|
+
|
|
17
|
+
**2. The server**, in whichever client you use:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
claude mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp # Claude Code
|
|
21
|
+
codex mcp add roblox-studio -- npx -y @el4cteo/rbx-studio-mcp # Codex CLI
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Same JSON block for **Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json`), **Claude Desktop** (`claude_desktop_config.json`), **Gemini CLI** (`~/.gemini/settings.json`) and **Windsurf** (`~/.codeium/windsurf/mcp_config.json`):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"roblox-studio": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**VS Code / Copilot** (`.vscode/mcp.json`):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"servers": {
|
|
42
|
+
"roblox-studio": {
|
|
43
|
+
"type": "stdio",
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "@el4cteo/rbx-studio-mcp"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**opencode** (`opencode.json`):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"$schema": "https://opencode.ai/config.json",
|
|
56
|
+
"mcp": {
|
|
57
|
+
"roblox-studio": {
|
|
58
|
+
"type": "local",
|
|
59
|
+
"command": ["npx", "-y", "@el4cteo/rbx-studio-mcp"],
|
|
60
|
+
"enabled": true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**3.** Open Studio and accept the `127.0.0.1` prompt — the plugin connects automatically. Verify with `studio_status`.
|
|
67
|
+
|
|
68
|
+
**4.** `debug` additionally needs **Debugger Luau API** in File → Beta Features, plus a Studio restart. Nothing else requires it.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
Port defaults to **44755** — change with `--port` or `ROBLOX_STUDIO_MCP_PORT`, and match it in the plugin widget. Loopback only.
|
|
73
|
+
|
|
74
|
+
## Multiple agents
|
|
75
|
+
|
|
76
|
+
Register the server in as many clients as you like — the plugin connects out to one port, the first server owns it and the rest proxy through. No configuration, no second Studio connection.
|
|
77
|
+
|
|
78
|
+
Each agent keeps its own target (`set_active_studio` is per client), so two agents can work on two open places and neither can retarget the other. Pass `studioId` on a single call to reach elsewhere without changing your default.
|
|
79
|
+
|
|
80
|
+
Subagents share their parent's connection and therefore its target — a subagent calling `set_active_studio` silently retargets its parent. Give subagents an explicit `studioId` per call.
|
|
81
|
+
|
|
82
|
+
## Tools
|
|
83
|
+
|
|
84
|
+
| | |
|
|
85
|
+
|---|---|
|
|
86
|
+
| **Session** | `studio_status` `list_studios` `set_active_studio` |
|
|
87
|
+
| **Discover** | `tree` `inspect` `find` `api` |
|
|
88
|
+
| **Scripts** | `script_read` `script_edit` `script_grep` `script_create` |
|
|
89
|
+
| **Instances** | `create` `modify` `delete` `move` |
|
|
90
|
+
| **World** | `geometry` `assets` `collision` `undo` |
|
|
91
|
+
| **Run & debug** | `playtest` `execute_luau` `character` `input` `console` `debug` `performance` |
|
|
92
|
+
| **Look** | `screenshot` `viewport` `device` |
|
|
93
|
+
|
|
94
|
+
Gotchas: during a playtest two sessions connect — pass `studioId` explicitly and use the edit session for anything that must persist. `device` emulation persists until `device op="stop"`.
|
|
95
|
+
|
|
96
|
+
## Batching
|
|
97
|
+
|
|
98
|
+
Every write tool takes an array — ten script edits or two hundred deletions is one call.
|
|
99
|
+
|
|
100
|
+
| tool | takes | cap |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `create` | instances, each nesting `children` to any depth | 100 |
|
|
103
|
+
| `modify` | entries, each with an unlimited list of `paths` | 100 entries |
|
|
104
|
+
| `delete` | paths | 200 |
|
|
105
|
+
| `move` | moves | 200 |
|
|
106
|
+
| `script_edit` | edits, across any number of scripts | 50 |
|
|
107
|
+
| `script_create` | scripts | 50 |
|
|
108
|
+
| `inspect` | paths | 50 |
|
|
109
|
+
| `input` | input steps, delivered in order | 40 |
|
|
110
|
+
|
|
111
|
+
`modify` caps *entries*, not targets — one entry can anchor five hundred parts, so pair it with `find` to change a whole place in one call.
|
|
112
|
+
|
|
113
|
+
Each batch is a single `ChangeHistoryService` recording: **one Ctrl+Z**. Batches are all-or-nothing — everything is transformed in memory first, so a failed match leaves the place untouched.
|
|
114
|
+
|
|
115
|
+
Parallel tool calls also work (responses are keyed by request id), but prefer a batch: N parallel calls are N round trips and N undo steps, a batch is one of each.
|
|
116
|
+
|
|
117
|
+
## Compared to what else exists
|
|
118
|
+
|
|
119
|
+
| | tools | transport | editor-safe writes | undo recording | live API dump | licence |
|
|
120
|
+
|---|---|---|---|---|---|---|
|
|
121
|
+
| **this** | 29 | **SSE push** | **yes** | **yes** | **yes** | MIT |
|
|
122
|
+
| [Roblox built-in](https://create.roblox.com/docs/studio/mcp) | ~27 | stdio | partial | — | n/a | closed source |
|
|
123
|
+
| [Chrrxs](https://github.com/Chrrxs/robloxstudio-mcp) | ~40 | poll | no | partial | no | MIT |
|
|
124
|
+
| [drgost1](https://github.com/drgost1/robloxstudio-mcp) | 51 | poll 500 ms | no | yes | no | MIT |
|
|
125
|
+
| [boshyxd](https://github.com/boshyxd/robloxstudio-mcp) | 43 | long-poll | no | no | no | MIT (archived) |
|
|
126
|
+
| [Roblox/studio-rust-mcp-server](https://github.com/Roblox/studio-rust-mcp-server) | 2 | HTTP | no | no | no | MIT (superseded) |
|
|
127
|
+
|
|
128
|
+
- **Push, not poll** — 50 sequential round trips: 13.6 ms mean vs 25.8 ms, 12.8 ms median vs 29.9 ms (`node scripts/latency.mjs --count 50 --compare`).
|
|
129
|
+
- **Safe script edits** — `ScriptEditorService:UpdateSourceAsync`, not `script.Source`; your unsaved editor buffer survives.
|
|
130
|
+
- **~16k tokens of schema** against 43–51 tools elsewhere. Cursor-paged, capped, `detail: concise | standard | full`.
|
|
131
|
+
- **Live API dump** — property typos get suggestions (`Anchorred` → `Anchored`).
|
|
132
|
+
|
|
133
|
+
Not built here: terrain, AI mesh and material generation.
|
|
134
|
+
|
|
135
|
+
## Security
|
|
136
|
+
|
|
137
|
+
Binds `127.0.0.1`, rejects `Origin`, and requires a header a browser cannot set cross-origin — closing the DNS-rebinding hole. HTTP permission is granted per plugin and per URL, so your experience's "Allow HTTP Requests" setting is untouched.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm install
|
|
143
|
+
npm run build # TypeScript -> dist/
|
|
144
|
+
npm run build:plugin # plugin/src -> build/StudioMCP.rbxmx
|
|
145
|
+
npm run install:plugin # build + copy into the Studio plugins folder
|
|
146
|
+
npm test # plugin (Luau) + bridge (Node) tests
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Needs `luau`, `luau-compile` and `luau-analyze` from [the Luau releases](https://github.com/luau-lang/luau/releases) on `PATH` or in `tools/`.
|
|
150
|
+
|
|
151
|
+
`evals/` holds ten questions answerable only by driving a real Studio session — see [evals/README.md](evals/README.md).
|
|
152
|
+
|
|
153
|
+
## Licence
|
|
154
|
+
|
|
155
|
+
MIT.
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { registerInputTools } from "./tools/input.js";
|
|
|
20
20
|
import { registerDeviceTools } from "./tools/device.js";
|
|
21
21
|
import { registerApiTools } from "./tools/api.js";
|
|
22
22
|
import { registerResources } from "./resources.js";
|
|
23
|
-
const VERSION = "0.1.
|
|
23
|
+
const VERSION = "0.1.6";
|
|
24
24
|
function parsePort(argv) {
|
|
25
25
|
const flag = argv.indexOf("--port");
|
|
26
26
|
const raw = flag !== -1 ? argv[flag + 1] : process.env["ROBLOX_STUDIO_MCP_PORT"];
|
package/dist/lib/errors.js
CHANGED
|
@@ -16,9 +16,11 @@ export class ToolError extends Error {
|
|
|
16
16
|
return new ToolError(error.code, error.message, error.hint);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
export const NO_STUDIO = () => new ToolError("NO_STUDIO", "No Roblox Studio instance is connected to this MCP server.", "Open Roblox Studio
|
|
20
|
-
"
|
|
21
|
-
"
|
|
19
|
+
export const NO_STUDIO = () => new ToolError("NO_STUDIO", "No Roblox Studio instance is connected to this MCP server.", "Open Roblox Studio with the companion plugin installed — it connects " +
|
|
20
|
+
"automatically on load. If it asks for permission to reach 127.0.0.1, " +
|
|
21
|
+
"accept it. Check connection status under the Studio MCP toolbar button, " +
|
|
22
|
+
"and use the reconnect button in that console if it is disconnected. " +
|
|
23
|
+
"Then retry.");
|
|
22
24
|
export const AMBIGUOUS_STUDIO = (names) => new ToolError("AMBIGUOUS_STUDIO", `${names.length} Studio instances are connected and the user has not said ` +
|
|
23
25
|
"which one to work in.", "Ask the user which place they mean, then call set_active_studio with its " +
|
|
24
26
|
`studioId. Connected now: ${names.join("; ")}. To act on one place without ` +
|
package/dist/lib/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,IAAI,CAAS;IACb,IAAI,CAAqB;IAElC,YAAY,IAAY,EAAE,OAAe,EAAE,IAAa;QACtD,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,MAAM,CAAC,gBAAgB,CAAC,KAAmB;QACzC,OAAO,IAAI,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9D,CAAC;CACF;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,GAAc,EAAE,CACvC,IAAI,SAAS,CACX,WAAW,EACX,4DAA4D,EAC5D,
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/session.js
CHANGED
|
@@ -21,11 +21,6 @@ export function registerSessionTools(context) {
|
|
|
21
21
|
"place or asking which file they mean. Studio exposes no focused-tab API, " +
|
|
22
22
|
"so with several open, prefer the one holding a selection and otherwise " +
|
|
23
23
|
"ask.\n\n" +
|
|
24
|
-
"`debuggerBeta: \"off\"` appears only when the Debugger Luau API beta is " +
|
|
25
|
-
"disabled, which is the default. `debug` cannot work until the user turns " +
|
|
26
|
-
"it on in File > Beta Features and restarts Studio — tell them rather than " +
|
|
27
|
-
"retrying, because nothing an agent can do will enable it. Everything else " +
|
|
28
|
-
"works regardless.\n\n" +
|
|
29
24
|
"Returns JSON. Selection is capped at 50 entries and selected text at 400 " +
|
|
30
25
|
"characters; use `find` or `script_read` for more.",
|
|
31
26
|
inputSchema: {
|
|
@@ -77,8 +72,8 @@ export function registerSessionTools(context) {
|
|
|
77
72
|
const sessions = view.list;
|
|
78
73
|
if (sessions.length === 0) {
|
|
79
74
|
return text("No Studio instances are connected.\n" +
|
|
80
|
-
"Open Roblox Studio
|
|
81
|
-
"
|
|
75
|
+
"Open Roblox Studio with the companion plugin installed — it connects " +
|
|
76
|
+
"automatically on load — then call this again.");
|
|
82
77
|
}
|
|
83
78
|
// A defaulted target is not reported as active: tools refuse to use it
|
|
84
79
|
// while several are connected, so showing it as active would explain
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/tools/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAoB,MAAM,gBAAgB,CAAC;AA+B9D,MAAM,UAAU,oBAAoB,CAAC,OAAoB;IACvD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAE3B,UAAU,CACR,OAAO,EACP;QACE,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE,eAAe;QACtB,WAAW,EACT,uEAAuE;YACvE,wEAAwE;YACxE,oEAAoE;YACpE,2EAA2E;YAC3E,uEAAuE;YACvE,yEAAyE;YACzE,0EAA0E;YAC1E,2EAA2E;YAC3E,4EAA4E;YAC5E,oEAAoE;YACpE,wEAAwE;YACxE,2EAA2E;YAC3E,yEAAyE;YACzE,UAAU;YACV,
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/tools/session.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@el4cteo/rbx-studio-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "MCP server for Roblox Studio. 29 tools, push-based SSE bridge, editor-safe script edits, one-step undo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
package/plugin/src/Config.luau
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
local Config = {}
|
|
11
11
|
|
|
12
|
-
Config.PLUGIN_VERSION = "0.1.
|
|
12
|
+
Config.PLUGIN_VERSION = "0.1.6"
|
|
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
|
|
@@ -45,16 +45,17 @@
|
|
|
45
45
|
--[[
|
|
46
46
|
Fetched defensively, because this module is required at plugin start.
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
nothing to read.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
Measured with the beta off and Studio restarted: `GetService` does NOT throw
|
|
49
|
+
here, it hands back a working service object. So this guard is not fixing an
|
|
50
|
+
observed crash -- it is insurance on the one call in this file that runs
|
|
51
|
+
before any tool is invoked. init.server.luau requires this module alongside
|
|
52
|
+
every other handler, so a throw at this line would take down all 29 tools
|
|
53
|
+
rather than the three that need a debugger, and it would do it silently: a
|
|
54
|
+
plugin that never loads never connects, leaving nothing to read anywhere.
|
|
55
|
+
|
|
56
|
+
The beta is NOT detected here, because nothing detects it cheaply. See the
|
|
57
|
+
note on the wholesale-refusal branch in `Debug.set` for the two probes that
|
|
58
|
+
were tried and why neither works.
|
|
58
59
|
]]
|
|
59
60
|
local ScriptDebuggerService: any = nil
|
|
60
61
|
do
|
|
@@ -322,6 +323,35 @@ function Debug.set(params: { [string]: any }): { [string]: any }
|
|
|
322
323
|
end
|
|
323
324
|
end
|
|
324
325
|
|
|
326
|
+
--[[
|
|
327
|
+
A wholesale refusal names the likely cause without asserting it.
|
|
328
|
+
|
|
329
|
+
`AddBreakpoint` is where the "Debugger Luau API" beta actually bites, and
|
|
330
|
+
its own message -- "Failed to execute AddBreakpoint request" -- names
|
|
331
|
+
nothing anyone can act on. The beta cannot be detected from here to say
|
|
332
|
+
so with certainty, and two attempts to are worth recording so they are
|
|
333
|
+
not tried a third time: with the beta OFF, `GetService`, `FindService`,
|
|
334
|
+
assigning `OnStopped` and `Enum.DebuggerResumeType` all still succeed;
|
|
335
|
+
with the beta ON, `ReflectionService` still does not list the class. The
|
|
336
|
+
first is always available and the second never is, so neither varies with
|
|
337
|
+
the thing being measured.
|
|
338
|
+
|
|
339
|
+
The only reliable test is this call, which cannot be run speculatively on
|
|
340
|
+
a user's script just to answer a status question. So the hint says what
|
|
341
|
+
to check first and what else it could be, and does not claim to know.
|
|
342
|
+
]]
|
|
343
|
+
if #added == 0 and #failed > 0 then
|
|
344
|
+
Dispatch.fail(
|
|
345
|
+
"NO_BREAKPOINTS_SET",
|
|
346
|
+
string.format("No breakpoint could be set; all %d were refused.", #failed),
|
|
347
|
+
"Check that \"Debugger Luau API\" is on in File > Beta Features and that "
|
|
348
|
+
.. "Studio has been restarted since -- that is the usual cause, it is "
|
|
349
|
+
.. "off by default, and only the user can change it. Otherwise the "
|
|
350
|
+
.. "line may not be one that runs: a `return`, an `end` or a bare "
|
|
351
|
+
.. "declaration is often refused, so try the statement above it."
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
325
355
|
return { added = added, failed = failed, installed = installed }
|
|
326
356
|
end
|
|
327
357
|
|
|
@@ -1,185 +1,169 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
Session and connectivity handlers. `studio.status` is the cheapest call in the
|
|
4
|
-
system and doubles as the liveness probe, so it must never touch anything
|
|
5
|
-
expensive -- notably it counts descendants lazily and caps the selection.
|
|
6
|
-
]]
|
|
7
|
-
|
|
8
|
-
local MarketplaceService = game:GetService("MarketplaceService")
|
|
9
|
-
local RunService = game:GetService("RunService")
|
|
10
|
-
local Selection = game:GetService("Selection")
|
|
11
|
-
|
|
12
|
-
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
13
|
-
local Context = require(script.Parent.Parent.Context)
|
|
14
|
-
local Editor = require(script.Parent.Parent.Editor)
|
|
15
|
-
local Emulation = require(script.Parent.Parent.Emulation)
|
|
16
|
-
local Paths = require(script.Parent.Parent.Paths)
|
|
17
|
-
|
|
18
|
-
-- `version()` is marked deprecated but remains the only way to read the Studio
|
|
19
|
-
-- build, and the agent needs it to know which engine APIs actually exist in this
|
|
20
|
-
-- session. Bound indirectly so the deprecation lint stays enabled file-wide.
|
|
21
|
-
local studioVersion: () -> string = (version :: any)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
--
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
local
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
local
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
local
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
local
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
only needs to know the channel is alive.
|
|
171
|
-
]]
|
|
172
|
-
function Session.ping(params: { [string]: any }): { [string]: any }
|
|
173
|
-
return { pong = true, echo = params.echo }
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
function Session.register()
|
|
177
|
-
Dispatch.registerAll("studio", {
|
|
178
|
-
status = function()
|
|
179
|
-
return Session.status()
|
|
180
|
-
end,
|
|
181
|
-
ping = Session.ping,
|
|
182
|
-
})
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
return Session
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Session and connectivity handlers. `studio.status` is the cheapest call in the
|
|
4
|
+
system and doubles as the liveness probe, so it must never touch anything
|
|
5
|
+
expensive -- notably it counts descendants lazily and caps the selection.
|
|
6
|
+
]]
|
|
7
|
+
|
|
8
|
+
local MarketplaceService = game:GetService("MarketplaceService")
|
|
9
|
+
local RunService = game:GetService("RunService")
|
|
10
|
+
local Selection = game:GetService("Selection")
|
|
11
|
+
|
|
12
|
+
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
13
|
+
local Context = require(script.Parent.Parent.Context)
|
|
14
|
+
local Editor = require(script.Parent.Parent.Editor)
|
|
15
|
+
local Emulation = require(script.Parent.Parent.Emulation)
|
|
16
|
+
local Paths = require(script.Parent.Parent.Paths)
|
|
17
|
+
|
|
18
|
+
-- `version()` is marked deprecated but remains the only way to read the Studio
|
|
19
|
+
-- build, and the agent needs it to know which engine APIs actually exist in this
|
|
20
|
+
-- session. Bound indirectly so the deprecation lint stays enabled file-wide.
|
|
21
|
+
local studioVersion: () -> string = (version :: any)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
-- Selection can be tens of thousands of instances after a rubber-band drag.
|
|
25
|
+
-- Anything past this is noise for an agent and would blow the response budget.
|
|
26
|
+
local MAX_SELECTION = 50
|
|
27
|
+
|
|
28
|
+
-- Counting descendants of a large place is O(n) over the whole DataModel, so it
|
|
29
|
+
-- is cached briefly; status is polled often enough for this to matter.
|
|
30
|
+
local COUNT_CACHE_SECONDS = 5
|
|
31
|
+
|
|
32
|
+
local cachedCounts: { descendants: number, scripts: number }? = nil
|
|
33
|
+
local cachedAt = 0
|
|
34
|
+
|
|
35
|
+
--[[
|
|
36
|
+
Which device Studio is pretending to be, or nil for none.
|
|
37
|
+
|
|
38
|
+
Reported here because nothing else says it. Device emulation resizes the
|
|
39
|
+
viewport and stays on until it is switched off, so every screenshot after it
|
|
40
|
+
comes back the wrong shape with no indication why -- and a caller who did not
|
|
41
|
+
set it, or who set it twenty calls ago, has no way to find out. `status` is
|
|
42
|
+
the call agents are told to make first, which makes it the right place for a
|
|
43
|
+
piece of session state that silently changes what everything else returns.
|
|
44
|
+
]]
|
|
45
|
+
local function counts(): { descendants: number, scripts: number }
|
|
46
|
+
local now = os.clock()
|
|
47
|
+
local cache = cachedCounts
|
|
48
|
+
if cache and now - cachedAt < COUNT_CACHE_SECONDS then
|
|
49
|
+
return cache
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
local descendants = 0
|
|
53
|
+
local scripts = 0
|
|
54
|
+
for _, instance in game:GetDescendants() do
|
|
55
|
+
descendants += 1
|
|
56
|
+
if instance:IsA("LuaSourceContainer") then
|
|
57
|
+
scripts += 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
local fresh = { descendants = descendants, scripts = scripts }
|
|
62
|
+
cachedCounts = fresh
|
|
63
|
+
cachedAt = now
|
|
64
|
+
return fresh
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
--[[
|
|
68
|
+
The name the user would recognise.
|
|
69
|
+
|
|
70
|
+
`game.Name` is the data model's name -- "Place1", "Place5" -- and has nothing
|
|
71
|
+
to do with what the place is called on the Studio tab or the Creator
|
|
72
|
+
Dashboard. Nothing local holds the real name: it lives on Roblox's servers, so
|
|
73
|
+
it takes a lookup. Without it, a user saying "the other place, the published
|
|
74
|
+
one" cannot be matched to any window, which is the whole point of being able
|
|
75
|
+
to drive several Studios at once.
|
|
76
|
+
|
|
77
|
+
Cached, including the failure, because an unpublished place would otherwise
|
|
78
|
+
make a doomed web request on every status call.
|
|
79
|
+
]]
|
|
80
|
+
local NAME_RETRY_SECONDS = 60
|
|
81
|
+
|
|
82
|
+
local resolvedName: string? = nil
|
|
83
|
+
local resolvedFor = -1
|
|
84
|
+
local resolvedAt = 0
|
|
85
|
+
|
|
86
|
+
local function publishedPlaceName(): string?
|
|
87
|
+
local placeId = game.PlaceId
|
|
88
|
+
-- A place that has never been saved to Roblox has no id and no remote name.
|
|
89
|
+
if placeId == 0 then
|
|
90
|
+
return nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
local fresh = resolvedFor == placeId
|
|
94
|
+
and (resolvedName ~= nil or os.clock() - resolvedAt < NAME_RETRY_SECONDS)
|
|
95
|
+
if fresh then
|
|
96
|
+
return resolvedName
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
resolvedFor = placeId
|
|
100
|
+
resolvedAt = os.clock()
|
|
101
|
+
|
|
102
|
+
local ok, info = pcall(function()
|
|
103
|
+
return MarketplaceService:GetProductInfo(placeId, Enum.InfoType.Asset)
|
|
104
|
+
end)
|
|
105
|
+
local name = if ok and typeof(info) == "table" then (info :: any).Name else nil
|
|
106
|
+
resolvedName = if typeof(name) == "string" and name ~= "" then name else nil
|
|
107
|
+
return resolvedName
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
local Session = {}
|
|
111
|
+
|
|
112
|
+
function Session.status(): { [string]: any }
|
|
113
|
+
local selected: { { path: string, className: string } } = {}
|
|
114
|
+
for index, instance in Selection:Get() do
|
|
115
|
+
if index > MAX_SELECTION then
|
|
116
|
+
break
|
|
117
|
+
end
|
|
118
|
+
table.insert(selected, { path = Paths.of(instance), className = instance.ClassName })
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
local totals = counts()
|
|
122
|
+
-- Open tabs ride along with status rather than getting their own tool: this
|
|
123
|
+
-- is the call agents are told to make first, and "which script is the user
|
|
124
|
+
-- looking at" is orientation, not a separate question.
|
|
125
|
+
local openScripts = Editor.documents()
|
|
126
|
+
|
|
127
|
+
local published = publishedPlaceName()
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
openScripts = if #openScripts > 0 then openScripts else nil,
|
|
131
|
+
placeName = published or (if game.Name ~= "" then game.Name else "Untitled place"),
|
|
132
|
+
-- Kept separate when the two differ, so a path rooted at the data model
|
|
133
|
+
-- name stays explicable next to a place called something else entirely.
|
|
134
|
+
dataModelName = if published and published ~= game.Name then game.Name else nil,
|
|
135
|
+
placeId = game.PlaceId,
|
|
136
|
+
context = Context.of(),
|
|
137
|
+
-- RunService:IsRunning() is true for both Play and Run; IsEdit()
|
|
138
|
+
-- distinguishes the authoring session from a live one.
|
|
139
|
+
isRunning = RunService:IsRunning(),
|
|
140
|
+
isEdit = RunService:IsEdit(),
|
|
141
|
+
isRunMode = RunService:IsRunning() and not RunService:IsClient(),
|
|
142
|
+
isServerView = RunService:IsServer(),
|
|
143
|
+
selection = selected,
|
|
144
|
+
selectionCount = #Selection:Get(),
|
|
145
|
+
descendantCount = totals.descendants,
|
|
146
|
+
scriptCount = totals.scripts,
|
|
147
|
+
studioVersion = studioVersion(),
|
|
148
|
+
emulatedDevice = Emulation.summary(),
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
--[[
|
|
153
|
+
Round-trip probe used by the smoke test and by `studio_status` when the agent
|
|
154
|
+
only needs to know the channel is alive.
|
|
155
|
+
]]
|
|
156
|
+
function Session.ping(params: { [string]: any }): { [string]: any }
|
|
157
|
+
return { pong = true, echo = params.echo }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
function Session.register()
|
|
161
|
+
Dispatch.registerAll("studio", {
|
|
162
|
+
status = function()
|
|
163
|
+
return Session.status()
|
|
164
|
+
end,
|
|
165
|
+
ping = Session.ping,
|
|
166
|
+
})
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return Session
|