@gjlmotea/studio-sidecar-mcp 0.0.0-stage → 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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -2
  3. package/README.zh-TW.md +233 -0
  4. package/dist/adapters/darwin-process-inspector.d.ts +27 -0
  5. package/dist/adapters/darwin-process-inspector.d.ts.map +1 -0
  6. package/dist/adapters/darwin-process-inspector.js +93 -0
  7. package/dist/adapters/darwin-process-inspector.js.map +1 -0
  8. package/dist/adapters/fetch-open-cloud.d.ts +22 -0
  9. package/dist/adapters/fetch-open-cloud.d.ts.map +1 -0
  10. package/dist/adapters/fetch-open-cloud.js +161 -0
  11. package/dist/adapters/fetch-open-cloud.js.map +1 -0
  12. package/dist/adapters/local-studio-host.d.ts +28 -0
  13. package/dist/adapters/local-studio-host.d.ts.map +1 -0
  14. package/dist/adapters/local-studio-host.js +131 -0
  15. package/dist/adapters/local-studio-host.js.map +1 -0
  16. package/dist/adapters/mcp-studio-bridge.d.ts +54 -0
  17. package/dist/adapters/mcp-studio-bridge.d.ts.map +1 -0
  18. package/dist/adapters/mcp-studio-bridge.js +206 -0
  19. package/dist/adapters/mcp-studio-bridge.js.map +1 -0
  20. package/dist/adapters/shell.d.ts +16 -0
  21. package/dist/adapters/shell.d.ts.map +1 -0
  22. package/dist/adapters/shell.js +37 -0
  23. package/dist/adapters/shell.js.map +1 -0
  24. package/dist/adapters/windows-process-inspector.d.ts +25 -0
  25. package/dist/adapters/windows-process-inspector.d.ts.map +1 -0
  26. package/dist/adapters/windows-process-inspector.js +105 -0
  27. package/dist/adapters/windows-process-inspector.js.map +1 -0
  28. package/dist/application/roblox-service.d.ts +160 -0
  29. package/dist/application/roblox-service.d.ts.map +1 -0
  30. package/dist/application/roblox-service.js +483 -0
  31. package/dist/application/roblox-service.js.map +1 -0
  32. package/dist/composition.d.ts +38 -0
  33. package/dist/composition.d.ts.map +1 -0
  34. package/dist/composition.js +172 -0
  35. package/dist/composition.js.map +1 -0
  36. package/dist/domain/asset-types.d.ts +33 -0
  37. package/dist/domain/asset-types.d.ts.map +1 -0
  38. package/dist/domain/asset-types.js +79 -0
  39. package/dist/domain/asset-types.js.map +1 -0
  40. package/dist/domain/contracts.d.ts +87 -0
  41. package/dist/domain/contracts.d.ts.map +1 -0
  42. package/dist/domain/contracts.js +2 -0
  43. package/dist/domain/contracts.js.map +1 -0
  44. package/dist/domain/extraction.d.ts +115 -0
  45. package/dist/domain/extraction.d.ts.map +1 -0
  46. package/dist/domain/extraction.js +258 -0
  47. package/dist/domain/extraction.js.map +1 -0
  48. package/dist/domain/launch-intent.d.ts +53 -0
  49. package/dist/domain/launch-intent.d.ts.map +1 -0
  50. package/dist/domain/launch-intent.js +92 -0
  51. package/dist/domain/launch-intent.js.map +1 -0
  52. package/dist/domain/luau-payloads.d.ts +42 -0
  53. package/dist/domain/luau-payloads.d.ts.map +1 -0
  54. package/dist/domain/luau-payloads.js +183 -0
  55. package/dist/domain/luau-payloads.js.map +1 -0
  56. package/dist/domain/path-containment.d.ts +13 -0
  57. package/dist/domain/path-containment.d.ts.map +1 -0
  58. package/dist/domain/path-containment.js +18 -0
  59. package/dist/domain/path-containment.js.map +1 -0
  60. package/dist/domain/revision.d.ts +25 -0
  61. package/dist/domain/revision.d.ts.map +1 -0
  62. package/dist/domain/revision.js +43 -0
  63. package/dist/domain/revision.js.map +1 -0
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +78 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/lifecycle.d.ts +42 -0
  69. package/dist/lifecycle.d.ts.map +1 -0
  70. package/dist/lifecycle.js +123 -0
  71. package/dist/lifecycle.js.map +1 -0
  72. package/dist/logger.d.ts +5 -0
  73. package/dist/logger.d.ts.map +1 -0
  74. package/dist/logger.js +6 -0
  75. package/dist/logger.js.map +1 -0
  76. package/dist/ports/open-cloud.d.ts +30 -0
  77. package/dist/ports/open-cloud.d.ts.map +1 -0
  78. package/dist/ports/open-cloud.js +11 -0
  79. package/dist/ports/open-cloud.js.map +1 -0
  80. package/dist/ports/process-inspector.d.ts +49 -0
  81. package/dist/ports/process-inspector.d.ts.map +1 -0
  82. package/dist/ports/process-inspector.js +14 -0
  83. package/dist/ports/process-inspector.js.map +1 -0
  84. package/dist/ports/studio-bridge.d.ts +33 -0
  85. package/dist/ports/studio-bridge.d.ts.map +1 -0
  86. package/dist/ports/studio-bridge.js +23 -0
  87. package/dist/ports/studio-bridge.js.map +1 -0
  88. package/dist/ports/studio-host.d.ts +27 -0
  89. package/dist/ports/studio-host.d.ts.map +1 -0
  90. package/dist/ports/studio-host.js +7 -0
  91. package/dist/ports/studio-host.js.map +1 -0
  92. package/dist/server/create-server.d.ts +13 -0
  93. package/dist/server/create-server.d.ts.map +1 -0
  94. package/dist/server/create-server.js +27 -0
  95. package/dist/server/create-server.js.map +1 -0
  96. package/dist/server/register-tools.d.ts +8 -0
  97. package/dist/server/register-tools.d.ts.map +1 -0
  98. package/dist/server/register-tools.js +339 -0
  99. package/dist/server/register-tools.js.map +1 -0
  100. package/dist/server/schemas.d.ts +285 -0
  101. package/dist/server/schemas.d.ts.map +1 -0
  102. package/dist/server/schemas.js +178 -0
  103. package/dist/server/schemas.js.map +1 -0
  104. package/dist/transports/stdio.d.ts +4 -0
  105. package/dist/transports/stdio.d.ts.map +1 -0
  106. package/dist/transports/stdio.js +7 -0
  107. package/dist/transports/stdio.js.map +1 -0
  108. package/package.json +68 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GJLMoTea
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 CHANGED
@@ -1,3 +1,233 @@
1
- # Temporary Holding Version
1
+ # Studio Sidecar — companion MCP server for Roblox Studio
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **English** | [繁體中文](https://github.com/gjlmotea/studio-sidecar-mcp/blob/main/README.zh-TW.md)
4
+
5
+ ![Studio Sidecar: a companion MCP server for Roblox Studio](https://github.com/gjlmotea/studio-sidecar-mcp/raw/main/assets/social-preview.png)
6
+
7
+ **Studio Sidecar fills the gaps in Roblox Studio's built-in MCP server. It does not replace it.**
8
+
9
+ Since 2026, Roblox Studio ships its own MCP server (`StudioMCP`, installed with Studio, stdio transport), and the standalone [`Roblox/studio-rust-mcp-server`](https://github.com/Roblox/studio-rust-mcp-server) repository has been archived. The built-in server has tools that no third party can rebuild: `generate_mesh`, `generate_material` and `generate_procedural_model` use Roblox's cloud AI with moderation; `search_asset`, `insert_asset` and `upload_image` carry your signed-in inventory permissions; `user_keyboard_input` and `user_mouse_input` go through `CreateVirtualInput`. Replacing it would throw all of that away.
10
+
11
+ So Studio Sidecar adds only what the built-in server structurally cannot do, and leaves everything else to it:
12
+
13
+ - **Open a place that is not open yet**, and see every Studio process and StudioMCP broker/stub on the machine.
14
+ - **Guard writes by PlaceId**, run Luau with runtime errors returned as readable values and output never truncated, and **extract scripts past the 100k output limit** with byte-exact verification.
15
+ - **Upload, update and archive assets through Roblox Open Cloud**, with an explicit verdict when an update was silently deduplicated.
16
+
17
+ No Studio plugin is needed. 11 tools; Windows fully verified, macOS partly (see [Platform support](#platform-support)).
18
+
19
+ ## Contents
20
+
21
+ - [Tools](#tools)
22
+ - [Quick start](#quick-start)
23
+ - [Configuration](#configuration)
24
+ - [What the tools know that the built-in server does not tell you](#what-the-tools-know-that-the-built-in-server-does-not-tell-you)
25
+ - [Platform support](#platform-support)
26
+ - [Boundaries](#boundaries)
27
+ - [Development](#development)
28
+ - [License and trademarks](#license-and-trademarks)
29
+
30
+ ## Tools
31
+
32
+ | Tool | Why the built-in server cannot do it |
33
+ |---|---|
34
+ | `roblox_get_status` | Shows whether Open Cloud is configured and which folders uploads may read from |
35
+ | `roblox_list_studios` | The built-in server lives inside one Studio process; it cannot see other Studio processes or the StudioMCP broker/stub structure |
36
+ | `roblox_open_place` | Opening a place that is not open yet is an operating-system job: it needs a new sibling process |
37
+ | `roblox_cleanup_stubs` | Same reason; reports by default and only terminates stubs with `apply=true`, never the broker |
38
+ | `roblox_place_guard` | The built-in `multi_edit` writes by path, and a wrong `studio_id` silently overwrites another game with no undo or warning |
39
+ | `roblox_luau_safe` | The built-in `execute_luau` has no PlaceId check and silently truncates output beyond 100k characters |
40
+ | `roblox_extract_place` | There is no tool that handles the 100k truncation boundary: chunking, sentinels and per-file byte checks are left to the caller, and getting them wrong does not raise an error |
41
+ | `roblox_get_asset` | The built-in `search_asset` reports the holder as `creatorId`, not the author |
42
+ | `roblox_upload_asset` | `AssetService:CreateAssetAsync` inside Studio currently always throws `not available yet` |
43
+ | `roblox_update_asset` | `CreateAssetVersionAsync` is unavailable in the same way |
44
+ | `roblox_archive_asset` | The built-in server has no Open Cloud surface |
45
+
46
+ **Deliberately not included**: script reading and writing, scene queries, `execute_luau`, `screen_capture`, playtesting. The built-in server already has them, and a second implementation would only drift.
47
+
48
+ ### How Luau runs on the Studio side
49
+
50
+ An MCP process can be a server and a client at the same time. Studio Sidecar starts `StudioMCP` as its own child process and connects to it as an MCP client, which gives it the built-in `execute_luau`. The chunking, sentinel and byte-check machinery is then wrapped into single tools. When the client closes normally, the child process goes away too, so this route does not leak stubs.
51
+
52
+ To start `StudioMCP`, Studio Sidecar uses the same launcher as Roblox's setup guide. On Windows that is `%LOCALAPPDATA%\Roblox\mcp.bat`, run through `cmd.exe /c`; when `mcp.bat` is missing, it falls back to the most recently modified `StudioMCP.exe` under `%LOCALAPPDATA%\Roblox\Versions\`. On macOS it uses `StudioMCP` inside `RobloxStudio.app`, in `/Applications` or `~/Applications`.
53
+
54
+ ## Quick start
55
+
56
+ Requirements: Roblox Studio with **Enable Studio as MCP server** turned on (in Studio: Assistant → … → **Manage MCP Servers**), and Node.js 22 or newer.
57
+
58
+ Register Studio Sidecar with your AI client. Claude Code:
59
+
60
+ ```bash
61
+ claude mcp add studio-sidecar --scope user \
62
+ -e ROBLOX_API_KEY="<your Open Cloud API key>" \
63
+ -e ROBLOX_CREATOR_USER_ID="<your Roblox user ID>" \
64
+ -e ROBLOX_UPLOAD_ROOT="<folder that uploads may read from>" \
65
+ -- npx -y @gjlmotea/studio-sidecar-mcp
66
+ ```
67
+
68
+ Clients that read an `mcpServers` JSON config (Claude Desktop, Cursor, Gemini CLI):
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "studio-sidecar": {
74
+ "command": "npx",
75
+ "args": ["-y", "@gjlmotea/studio-sidecar-mcp"],
76
+ "env": {
77
+ "ROBLOX_API_KEY": "<your Open Cloud API key>",
78
+ "ROBLOX_CREATOR_USER_ID": "<your Roblox user ID>",
79
+ "ROBLOX_UPLOAD_ROOT": "<folder that uploads may read from>"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ VS Code uses a top-level `servers` key instead, and each server has a `type`. An input variable keeps the key out of the file: VS Code asks for it the first time the server starts and stores it securely.
87
+
88
+ ```json
89
+ {
90
+ "inputs": [
91
+ {
92
+ "type": "promptString",
93
+ "id": "roblox-api-key",
94
+ "description": "Roblox Open Cloud API key",
95
+ "password": true
96
+ }
97
+ ],
98
+ "servers": {
99
+ "studio-sidecar": {
100
+ "type": "stdio",
101
+ "command": "npx",
102
+ "args": ["-y", "@gjlmotea/studio-sidecar-mcp"],
103
+ "env": {
104
+ "ROBLOX_API_KEY": "${input:roblox-api-key}",
105
+ "ROBLOX_CREATOR_USER_ID": "<your Roblox user ID>",
106
+ "ROBLOX_UPLOAD_ROOT": "<folder that uploads may read from>"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ Where each client keeps the file:
114
+
115
+ | Client | Config file |
116
+ |---|---|
117
+ | Claude Desktop | macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`; Windows: `%APPDATA%\Claude\claude_desktop_config.json`. **Settings → Developer → Edit Config** opens it |
118
+ | Cursor | `~/.cursor/mcp.json` for every project, `.cursor/mcp.json` inside one project |
119
+ | Gemini CLI | `~/.gemini/settings.json` (user), `.gemini/settings.json` (project) |
120
+ | VS Code | `.vscode/mcp.json` in the workspace, or the user `mcp.json` that the **MCP: Open User Configuration** command opens |
121
+
122
+ Sources: [Claude Desktop](https://modelcontextprotocol.io/docs/develop/connect-local-servers), [Cursor](https://cursor.com/docs/context/mcp), [Gemini CLI](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md), [VS Code](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration).
123
+
124
+ All three variables are optional: without the key the four Open Cloud tools refuse, without `ROBLOX_UPLOAD_ROOT` every upload is refused, and the other tools work as usual. **Do not put the key in any config file that is tracked by version control.**
125
+
126
+ Register Studio's built-in MCP server as well; script editing, screenshots and playtesting live there. The launch command on each platform, `StudioMCP` on macOS and `cmd.exe /c %LOCALAPPDATA%\Roblox\mcp.bat` on Windows, comes from Roblox's [setup guide](https://create.roblox.com/docs/studio/mcp).
127
+
128
+ macOS:
129
+
130
+ ```bash
131
+ claude mcp add roblox-studio --scope user -- /Applications/RobloxStudio.app/Contents/MacOS/StudioMCP
132
+ ```
133
+
134
+ Windows:
135
+
136
+ ```bat
137
+ claude mcp add roblox-studio --scope user -- cmd.exe /c "%LOCALAPPDATA%\Roblox\mcp.bat"
138
+ ```
139
+
140
+ In a JSON config, the macOS entry is `"command": "/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP"`, and the Windows entry is `"command": "cmd.exe"` with `"args": ["/c", "%LOCALAPPDATA%\\Roblox\\mcp.bat"]`. Register `mcp.bat` rather than `StudioMCP.exe` on Windows: `mcp.bat` keeps its path, while `StudioMCP.exe` lives in `%LOCALAPPDATA%\Roblox\Versions\version-<hash>\`, a folder that changes with Studio updates.
141
+
142
+ Restart the session after registering, then call `roblox_get_status` and check `openCloudConfigured` and `allowedUploadRoots`.
143
+
144
+ Desktop apps started from Finder or the Start menu often do not see your shell's `PATH`; if the server does not start, use the absolute path of `npx` (or of `node` when running from source).
145
+
146
+ ### Running from source
147
+
148
+ ```bash
149
+ git clone https://github.com/gjlmotea/studio-sidecar-mcp.git
150
+ cd studio-sidecar-mcp
151
+ corepack pnpm install --frozen-lockfile
152
+ corepack pnpm run build
153
+ claude mcp add studio-sidecar --scope user -- "$(which node)" "$PWD/dist/index.js"
154
+ ```
155
+
156
+ Register with absolute paths, as above, rather than a hand-written `.mcp.json` that points at the project folder. Claude Code sets `CLAUDE_PROJECT_DIR` in the server's environment, not in its own, so a `${CLAUDE_PROJECT_DIR}` reference in the `command` or `args` of a `.mcp.json` entry, or of a local- or user-scope entry in `~/.claude.json`, needs a default such as `${CLAUDE_PROJECT_DIR:-.}`. Without a default the entry still loads, `claude mcp list` shows a missing-variable warning, and the unexpanded `${CLAUDE_PROJECT_DIR}` text is used as is. Project-scope servers from `.mcp.json` also need approval: interactive sessions ask before using them, while `claude -p` runs, Agent SDK sessions and cloud sessions load them without asking. See the [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp).
157
+
158
+ ## Configuration
159
+
160
+ | Variable | Purpose |
161
+ |---|---|
162
+ | `ROBLOX_API_KEY` | Open Cloud API key with Asset **read and write** (Creator Dashboard → Open Cloud API Keys) |
163
+ | `ROBLOX_CREATOR_USER_ID` | Creator recorded in `creationContext` when uploading |
164
+ | `ROBLOX_UPLOAD_ROOT` | **Folders uploads may read from**. Several are allowed, joined with the platform's path delimiter: `;` on Windows (`C:\assets\models;D:\decals`), `:` on macOS (`/Users/you/models:/Volumes/art/decals`). **When unset, every upload is refused** |
165
+ | `ROBLOX_STUDIO_PATH` | Studio executable; resolved automatically from the current installation when unset |
166
+
167
+ `ROBLOX_UPLOAD_ROOT` is a deliberate guard rail: uploading sends a local file to an external service, and for Models it cannot be undone, so the MCP client may not pick arbitrary file paths.
168
+
169
+ ## What the tools know that the built-in server does not tell you
170
+
171
+ Each of these was found on real hardware and has a regression test.
172
+
173
+ **Launch intent quoting fails silently.** Studio expects `-launchIntentString "{\"task\":\"EditPlace\",\"universeid\":\"…\",\"placeid\":\"…\"}"`. Unescaped quotes are eaten by the Windows command-line parser, Studio receives broken JSON, ignores the intent and opens an empty session with no error. The only clue is a window title of `Roblox Studio` instead of `<place name> - Roblox Studio`. Studio Sidecar passes arguments to `spawn` as an array and makes the `CommandLineToArgvW` rules testable. `universeid` is the `gameId` in `scene/place.json`.
174
+
175
+ **Never steal window focus.** Two seconds after `SetForegroundWindow`, a stray key press in Studio closed the place that had just loaded. `roblox_open_place` only polls the broker connection and process titles.
176
+
177
+ **The broker must not be killed, and its identity moves.** StudioMCP is one broker plus one stub per client; the first one listens on `127.0.0.1:13469`, and Studio itself and every stub connect to it. Killing the broker drops every connection, Studio's included. The broker role also moves to a different process over time, so the broker is always identified as "whoever listens on 13469", never by a remembered PID. `roblox_cleanup_stubs` gives up when it cannot identify the broker. Stubs pile up because some clients do not reap them on exit (see [openai/codex#25893](https://github.com/openai/codex/issues/25893)); an age threshold keeps cleanup away from sessions that may still be alive.
178
+
179
+ **An update succeeds by `revisionId`, not by HTTP status.** PATCHing an asset with byte-identical content returns `HTTP 200`, `done: true` and no error, but `revisionId` does not move: the content was deduplicated. `roblox_update_asset` compares `revisionId` before and after and reports `updated`, `deduplicated` or `indeterminate`. A higher `revisionId` still does not guarantee that Studio sees the new content; Studio-side caching is outside what Open Cloud can confirm. To replace content, do not send `updateMask` (`updateMask=fileContent` fails with `Unknown field file_content`); it is only for metadata changes.
180
+
181
+ **The 100k truncation belongs to StudioMCP itself.** Measured directly against StudioMCP: a 100,000-character reply arrives whole; a 120,000-character one stops at 100,015 characters ending in `... (truncated)`. No client can lift it. `roblox_extract_place` therefore uses three safeguards, all required:
182
+
183
+ 1. A sentinel at the end of every chunk; a chunk without it counts as incomplete. Length alone cannot tell a complete reply from a truncated one.
184
+ 2. The UTF-8-safe cut point is decided in Lua and reported back, and the caller continues from there, so the two sides never compute boundaries separately.
185
+ 3. A byte-count check per file (`Buffer.byteLength`, not `String.length`). Any mismatch aborts the whole extraction, because a partially correct snapshot looks like success.
186
+
187
+ Each call stages its data under its own key, `shared.RBXEXTRACT_<32 hex digits>` (Lua VM memory only, never the DataModel or the `.rbxl`), and clears it afterwards, so overlapping calls never read each other's data. Instance names can be almost anything, including `..` and path separators, so every path segment is sanitized and the final path is checked to be inside the output folder before writing.
188
+
189
+ Files are created exclusively and never overwrite anything. When a name is already taken, including by a name the file system treats as equal (`Σ`, `ς` and `σ` on a case-insensitive volume), the script gets the next `~n` suffix, and `scene/scripts.json` records the path actually written. Re-running into the same folder first deletes the files listed in that folder's previous `scene/scripts.json`, and nothing else; a folder whose `scene/` files were not written by Studio Sidecar is refused.
190
+
191
+ **The Studio bridge must live as long as the server.** Closing it after every call makes the first call succeed and every later one fail with "Unable to reach Roblox Studio right now", which sounds like Studio is closed. It is not: closing kills the stub, and the next call races the broker. The same message also appears during a cold start before a fresh stub reaches the broker, so the bridge retries the Studio listing on that one message, and only that one, for up to 20 seconds; the wait grows by 250 ms per attempt, up to 1.5 seconds. Once the server starts shutting down, the bridge refuses new connections, so a call that is still finishing cannot start a fresh stub.
192
+
193
+ **Open Cloud formats are narrow.** Models accept `.fbx` only; Decals accept bmp, jpeg, png and tga; Audio accepts mp3 and ogg and can only be created, not updated, under a monthly quota. Wrong formats would only fail after polling the operation, so they are rejected locally. Models and Decals have never supported archiving, and `roblox_archive_asset` says so up front.
194
+
195
+ ## Platform support
196
+
197
+ | | Windows | macOS |
198
+ |---|:---:|:---:|
199
+ | Open a given place | ✅ verified | ✅ implemented, **not verified on hardware** |
200
+ | Process and broker inspection | ✅ verified (PowerShell) | ✅ verified (`ps` + `lsof`) |
201
+ | Stub cleanup | ✅ verified | ✅ implemented, **not verified on hardware** |
202
+ | Window titles | ✅ | ❌ always `undefined` (needs Accessibility permission) |
203
+ | Open Cloud tools | ✅ | ✅ (plain HTTP) |
204
+
205
+ Platform differences sit behind `ports/process-inspector.ts`; broker detection, stub classification and cleanup are shared. Linux is refused explicitly rather than returning an empty snapshot, because Roblox Studio does not run there and an empty answer would read as "no Studio running".
206
+
207
+ ## Boundaries
208
+
209
+ - **No window focus changes.** Stealing focus can make a stray key close a freshly loaded place.
210
+ - **`roblox_luau_safe` is not a new way to run arbitrary code.** `execute_luau` is already in the client's hands; this tool adds a mandatory PlaceId guard, one `pcall` around the whole chunk and chunked results in front of the same entry point. The `pcall` catches a runtime error and returns it as a readable value; the code after the error does not run, and changes made before the error are not rolled back. It is not a sandbox.
211
+ - **No reverse engineering of the broker's private protocol on 13469.** Talking to `StudioMCP` as an ordinary MCP client makes that unnecessary.
212
+ - **No "remove from inventory".** That goes through `inventory.roblox.com` with a signed-in session cookie and CSRF token, which an Open Cloud API key cannot do.
213
+ - **Known limitation: a folder on the output path can be swapped between the check and the file operation.** Each folder level is resolved with `realpath` and checked to be inside the output folder before a file is written, or a file from the previous extraction is deleted, but another program on the same machine could replace a folder with a symlink in between (time-of-check/time-of-use). The final file is created with `O_EXCL`, so an existing file or symlink at that name is never opened; `O_NOFOLLOW` is added where the platform has it, and Windows (libuv) does not support it.
214
+ - **Known limitation: a failed buffer release leaves the key behind.** If releasing a Luau buffer fails, or is skipped because the server is shutting down, that call's `shared.RBXEXTRACT_…` key stays in Studio's `shared` table. There is no retry and no expiry.
215
+
216
+ ## Development
217
+
218
+ ```bash
219
+ corepack pnpm install
220
+ corepack pnpm run verify # typecheck, tests, build, stdio smoke
221
+ corepack pnpm run verify:live # read-only checks against real Studio and a real account
222
+ corepack pnpm run verify:extract # end to end: open_place → place_guard → luau_safe → extract_place
223
+ ```
224
+
225
+ `verify:live` only performs read-only operations: no uploads, updates, opened places or terminated processes. Its Open Cloud part needs `ROBLOX_API_KEY` and an asset ID (`node scripts/verify-live.mjs <assetId>`) and is skipped otherwise.
226
+
227
+ `verify:extract` needs `ROBLOX_VERIFY_PLACE_ID`, `ROBLOX_VERIFY_UNIVERSE_ID` (a place you own) and `ROBLOX_VERIFY_BASELINE` (a hand-made, byte-verified script snapshot). It opens a Studio instance and compares the extracted scripts byte for byte against the snapshot. The steps are chained on purpose, because the bridge lifetime bug only shows up across several consecutive tool calls.
228
+
229
+ ## License and trademarks
230
+
231
+ [MIT](https://github.com/gjlmotea/studio-sidecar-mcp/blob/main/LICENSE).
232
+
233
+ Studio Sidecar is an independent open-source project, not affiliated with or endorsed by Roblox Corporation. Roblox, Roblox Studio and Luau are trademarks of Roblox Corporation.
@@ -0,0 +1,233 @@
1
+ # Studio Sidecar — Roblox Studio 的補完型 MCP server
2
+
3
+ [English](https://github.com/gjlmotea/studio-sidecar-mcp/blob/main/README.md) | **繁體中文**
4
+
5
+ ![Studio Sidecar:Roblox Studio 的補完型 MCP server](https://github.com/gjlmotea/studio-sidecar-mcp/raw/main/assets/social-preview.png)
6
+
7
+ **Studio Sidecar 補上 Roblox Studio 內建 MCP server 做不到的部分,不是要取代它。**
8
+
9
+ Roblox Studio 從 2026 年起內建自己的 MCP server(`StudioMCP`,隨 Studio 安裝,走 stdio),獨立的 [`Roblox/studio-rust-mcp-server`](https://github.com/Roblox/studio-rust-mcp-server) repository 也已封存。內建 server 有一些第三方無法重做的工具:`generate_mesh`、`generate_material` 與 `generate_procedural_model` 使用 Roblox 的雲端 AI,並經過內容審核;`search_asset`、`insert_asset` 與 `upload_image` 帶著你登入後的物品欄權限;`user_keyboard_input` 與 `user_mouse_input` 走 `CreateVirtualInput`。取代它就等於把這些全部丟掉。
10
+
11
+ 所以 Studio Sidecar 只補內建 server 結構上做不到的事,其餘一律交給它:
12
+
13
+ - **開啟還沒開的 place**,並看到這台機器上所有 Studio 行程與 StudioMCP 的 broker/stub。
14
+ - **依 PlaceId 守住寫入**、執行 Luau 時把執行期錯誤變成可讀的回傳值且輸出不被截斷,以及**突破 100k 輸出上限抽取腳本**,並逐位元組校驗。
15
+ - **透過 Roblox Open Cloud 上傳、更新與封存資產**;更新被靜默去重時會明確回報判定結果。
16
+
17
+ 不需要 Studio 外掛。共 11 個工具;Windows 已完整驗證,macOS 部分驗證(見[平台支援](#平台支援))。
18
+
19
+ ## 目錄
20
+
21
+ - [工具](#工具)
22
+ - [快速開始](#快速開始)
23
+ - [設定](#設定)
24
+ - [內建 server 沒告訴你的事](#內建-server-沒告訴你的事)
25
+ - [平台支援](#平台支援)
26
+ - [邊界](#邊界)
27
+ - [開發](#開發)
28
+ - [授權與商標](#授權與商標)
29
+
30
+ ## 工具
31
+
32
+ | 工具 | 內建 server 為什麼做不到 |
33
+ |---|---|
34
+ | `roblox_get_status` | 顯示 Open Cloud 是否已設定,以及上傳可以讀取哪些資料夾 |
35
+ | `roblox_list_studios` | 內建 server 活在某一個 Studio 行程裡,看不到其他 Studio 行程,也看不到 StudioMCP 的 broker/stub 結構 |
36
+ | `roblox_open_place` | 開啟還沒開的 place 是作業系統層級的事:需要另起一個同層級的行程 |
37
+ | `roblox_cleanup_stubs` | 理由同上;預設只回報,只有 `apply=true` 才終止 stub,而且絕不動 broker |
38
+ | `roblox_place_guard` | 內建的 `multi_edit` 依路徑寫入,`studio_id` 傳錯就會靜默覆蓋另一個遊戲,沒有復原也沒有警告 |
39
+ | `roblox_luau_safe` | 內建的 `execute_luau` 沒有 PlaceId 檢查,超過 100k 字元的輸出會被靜默截斷 |
40
+ | `roblox_extract_place` | 沒有工具處理 100k 截斷邊界:分塊、哨兵與逐檔位元組校驗都留給呼叫端,而做錯也不會報錯 |
41
+ | `roblox_get_asset` | 內建的 `search_asset` 回報的 `creatorId` 是持有者,不是作者 |
42
+ | `roblox_upload_asset` | Studio 裡的 `AssetService:CreateAssetAsync` 目前一律拋出 `not available yet` |
43
+ | `roblox_update_asset` | `CreateAssetVersionAsync` 同樣無法使用 |
44
+ | `roblox_archive_asset` | 內建 server 沒有任何 Open Cloud 功能 |
45
+
46
+ **刻意不包含**:腳本讀寫、場景查詢、`execute_luau`、`screen_capture`、playtest。內建 server 已經有這些,再做一份只會讓兩套實作漸漸不一致。
47
+
48
+ ### Luau 怎麼在 Studio 端執行
49
+
50
+ 一個 MCP 行程可以同時是 server 和 client。Studio Sidecar 把 `StudioMCP` 當成自己的子行程啟動,再以 MCP client 身分連上它,藉此取得內建的 `execute_luau`。分塊、哨兵與位元組校驗的機制則包成單一工具。client 正常關閉時,子行程也會跟著結束,所以這條路不會留下 stub。
51
+
52
+ 啟動 `StudioMCP` 時,Studio Sidecar 用的是 Roblox 設定說明裡的同一支啟動器。Windows 是 `%LOCALAPPDATA%\Roblox\mcp.bat`,經 `cmd.exe /c` 執行;找不到 `mcp.bat` 時,退回 `%LOCALAPPDATA%\Roblox\Versions\` 底下修改時間最新的 `StudioMCP.exe`。macOS 則用 `/Applications` 或 `~/Applications` 裡 `RobloxStudio.app` 內的 `StudioMCP`。
53
+
54
+ ## 快速開始
55
+
56
+ 需求:Roblox Studio 已開啟 **Enable Studio as MCP server**(Studio 裡:Assistant → … → **Manage MCP Servers**),以及 Node.js 22 以上。
57
+
58
+ 在 AI client 註冊 Studio Sidecar。Claude Code:
59
+
60
+ ```bash
61
+ claude mcp add studio-sidecar --scope user \
62
+ -e ROBLOX_API_KEY="<你的 Open Cloud API 金鑰>" \
63
+ -e ROBLOX_CREATOR_USER_ID="<你的 Roblox user ID>" \
64
+ -e ROBLOX_UPLOAD_ROOT="<允許上傳讀取的資料夾>" \
65
+ -- npx -y @gjlmotea/studio-sidecar-mcp
66
+ ```
67
+
68
+ 讀 `mcpServers` JSON 設定的 client(Claude Desktop、Cursor、Gemini CLI):
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "studio-sidecar": {
74
+ "command": "npx",
75
+ "args": ["-y", "@gjlmotea/studio-sidecar-mcp"],
76
+ "env": {
77
+ "ROBLOX_API_KEY": "<你的 Open Cloud API 金鑰>",
78
+ "ROBLOX_CREATOR_USER_ID": "<你的 Roblox user ID>",
79
+ "ROBLOX_UPLOAD_ROOT": "<允許上傳讀取的資料夾>"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ VS Code 改用最上層的 `servers` 鍵,每個 server 都要寫 `type`。用 input 變數就不必把金鑰寫進檔案:VS Code 會在 server 第一次啟動時詢問,並安全地保存起來。
87
+
88
+ ```json
89
+ {
90
+ "inputs": [
91
+ {
92
+ "type": "promptString",
93
+ "id": "roblox-api-key",
94
+ "description": "Roblox Open Cloud API key",
95
+ "password": true
96
+ }
97
+ ],
98
+ "servers": {
99
+ "studio-sidecar": {
100
+ "type": "stdio",
101
+ "command": "npx",
102
+ "args": ["-y", "@gjlmotea/studio-sidecar-mcp"],
103
+ "env": {
104
+ "ROBLOX_API_KEY": "${input:roblox-api-key}",
105
+ "ROBLOX_CREATOR_USER_ID": "<你的 Roblox user ID>",
106
+ "ROBLOX_UPLOAD_ROOT": "<允許上傳讀取的資料夾>"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ 各 client 的設定檔位置:
114
+
115
+ | Client | 設定檔 |
116
+ |---|---|
117
+ | Claude Desktop | macOS:`~/Library/Application Support/Claude/claude_desktop_config.json`;Windows:`%APPDATA%\Claude\claude_desktop_config.json`。**Settings → Developer → Edit Config** 會開啟它 |
118
+ | Cursor | 所有專案共用 `~/.cursor/mcp.json`,單一專案用專案內的 `.cursor/mcp.json` |
119
+ | Gemini CLI | `~/.gemini/settings.json`(user)、`.gemini/settings.json`(project) |
120
+ | VS Code | 工作區的 `.vscode/mcp.json`,或用 **MCP: Open User Configuration** 指令開啟的使用者 `mcp.json` |
121
+
122
+ 出處:[Claude Desktop](https://modelcontextprotocol.io/docs/develop/connect-local-servers)、[Cursor](https://cursor.com/docs/context/mcp)、[Gemini CLI](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md)、[VS Code](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration)。
123
+
124
+ 三個變數都可以省略:沒有金鑰時四個 Open Cloud 工具會拒絕,沒有 `ROBLOX_UPLOAD_ROOT` 時所有上傳一律拒絕,其他工具照常運作。**金鑰不要寫進任何受版本控制追蹤的設定檔。**
125
+
126
+ Studio 內建的 MCP server 也要註冊;腳本編輯、截圖與 playtest 都在那邊。各平台的啟動指令(macOS 是 `StudioMCP`,Windows 是 `cmd.exe /c %LOCALAPPDATA%\Roblox\mcp.bat`)取自 Roblox 的[設定說明](https://create.roblox.com/docs/studio/mcp)。
127
+
128
+ macOS:
129
+
130
+ ```bash
131
+ claude mcp add roblox-studio --scope user -- /Applications/RobloxStudio.app/Contents/MacOS/StudioMCP
132
+ ```
133
+
134
+ Windows:
135
+
136
+ ```bat
137
+ claude mcp add roblox-studio --scope user -- cmd.exe /c "%LOCALAPPDATA%\Roblox\mcp.bat"
138
+ ```
139
+
140
+ JSON 設定裡,macOS 寫 `"command": "/Applications/RobloxStudio.app/Contents/MacOS/StudioMCP"`;Windows 寫 `"command": "cmd.exe"` 加上 `"args": ["/c", "%LOCALAPPDATA%\\Roblox\\mcp.bat"]`。Windows 上要註冊 `mcp.bat` 而不是 `StudioMCP.exe`:`mcp.bat` 的路徑固定,`StudioMCP.exe` 則放在 `%LOCALAPPDATA%\Roblox\Versions\version-<hash>\`,這個資料夾會隨 Studio 更新而換。
141
+
142
+ 註冊後重開 session,再呼叫 `roblox_get_status`,確認 `openCloudConfigured` 與 `allowedUploadRoots`。
143
+
144
+ 從 Finder 或開始選單啟動的桌面 app 常讀不到 shell 的 `PATH`;server 起不來時,改用 `npx` 的絕對路徑(從原始碼執行時則是 `node` 的絕對路徑)。
145
+
146
+ ### 從原始碼執行
147
+
148
+ ```bash
149
+ git clone https://github.com/gjlmotea/studio-sidecar-mcp.git
150
+ cd studio-sidecar-mcp
151
+ corepack pnpm install --frozen-lockfile
152
+ corepack pnpm run build
153
+ claude mcp add studio-sidecar --scope user -- "$(which node)" "$PWD/dist/index.js"
154
+ ```
155
+
156
+ 請像上面這樣用絕對路徑註冊,不要手寫一份指向專案資料夾的 `.mcp.json`。Claude Code 把 `CLAUDE_PROJECT_DIR` 設在 server 的環境裡,而不是它自己的環境,所以在 `.mcp.json` 項目、或 `~/.claude.json` 裡 local/user scope 項目的 `command` 或 `args` 引用 `${CLAUDE_PROJECT_DIR}` 時,必須帶預設值,例如 `${CLAUDE_PROJECT_DIR:-.}`。沒有預設值時,該項目照樣載入,`claude mcp list` 會顯示缺少變數的警告,並原樣使用未展開的 `${CLAUDE_PROJECT_DIR}` 字串。`.mcp.json` 的 project scope server 還需要核准:互動 session 會先詢問,`claude -p`、Agent SDK session 與雲端 session 則不詢問直接載入。見 [Claude Code 的 MCP 文件](https://code.claude.com/docs/en/mcp)。
157
+
158
+ ## 設定
159
+
160
+ | 變數 | 用途 |
161
+ |---|---|
162
+ | `ROBLOX_API_KEY` | Open Cloud API 金鑰,需要 Asset 的**讀取與寫入**權限(Creator Dashboard → Open Cloud API Keys) |
163
+ | `ROBLOX_CREATOR_USER_ID` | 上傳時記錄在 `creationContext` 的建立者 |
164
+ | `ROBLOX_UPLOAD_ROOT` | **上傳可以讀取的資料夾**。可以有多個,以平台的路徑分隔符連接:Windows 用 `;`(`C:\assets\models;D:\decals`),macOS 用 `:`(`/Users/you/models:/Volumes/art/decals`)。**未設定時所有上傳一律拒絕** |
165
+ | `ROBLOX_STUDIO_PATH` | Studio 執行檔;未設定時自動從目前的安裝解析 |
166
+
167
+ `ROBLOX_UPLOAD_ROOT` 是刻意設下的護欄:上傳會把本機檔案送到外部服務,對 Model 來說還無法撤回,所以不讓 MCP client 任意指定檔案路徑。
168
+
169
+ ## 內建 server 沒告訴你的事
170
+
171
+ 以下每一條都是在真機上發現的,而且都有回歸測試。
172
+
173
+ **啟動意圖的引號錯了不會有任何錯誤。** Studio 要的是 `-launchIntentString "{\"task\":\"EditPlace\",\"universeid\":\"…\",\"placeid\":\"…\"}"`。沒有跳脫的引號會被 Windows 命令列解析吃掉,Studio 收到壞掉的 JSON,就忽略整個 intent、開一個空白 session,而且不報錯。唯一的線索是視窗標題停在 `Roblox Studio`,而不是 `<place 名稱> - Roblox Studio`。Studio Sidecar 把引數以陣列交給 `spawn`,並讓 `CommandLineToArgvW` 的規則可以測試。`universeid` 就是 `scene/place.json` 裡的 `gameId`。
174
+
175
+ **絕不搶視窗焦點。** `SetForegroundWindow` 之後兩秒,一個落進 Studio 的游離按鍵就把剛載好的 place 關掉了。`roblox_open_place` 只輪詢 broker 連線與行程標題。
176
+
177
+ **broker 不能殺,而且它的身分會換。** StudioMCP 是一個 broker 加上每個 client 一隻 stub;最先啟動的那個監聽 `127.0.0.1:13469`,Studio 本身與每隻 stub 都連到它。殺掉 broker 會斷掉所有連線,包括 Studio 自己的。broker 的角色也會隨時間換到別的行程,所以一律用「誰在監聽 13469」來認 broker,從不靠記下來的 PID。`roblox_cleanup_stubs` 認不出 broker 時就放棄。stub 會累積,是因為有些 client 結束時不回收它們(見 [openai/codex#25893](https://github.com/openai/codex/issues/25893));存活時間門檻讓清理不去碰可能還活著的 session。
178
+
179
+ **更新成功與否看 `revisionId`,不看 HTTP 狀態。** 用位元組完全相同的內容 PATCH 資產,會回 `HTTP 200`、`done: true` 且沒有錯誤,但 `revisionId` 不動:內容被去重了。`roblox_update_asset` 比對更新前後的 `revisionId`,回報 `updated`、`deduplicated` 或 `indeterminate`。`revisionId` 增加也不保證 Studio 看得到新內容;Studio 端的快取不在 Open Cloud 能確認的範圍內。換內容時不要帶 `updateMask`(`updateMask=fileContent` 會失敗並回 `Unknown field file_content`),它只用於修改中繼資料。
180
+
181
+ **100k 截斷是 StudioMCP 自己的限制。** 直接對 StudioMCP 量測:100,000 字元的回覆完整送達;120,000 字元的回覆停在 100,015 字元,結尾是 `... (truncated)`。沒有任何 client 能解除這個限制。因此 `roblox_extract_place` 用三道防護,缺一不可:
182
+
183
+ 1. 每塊結尾放一個哨兵;沒有哨兵的塊視為不完整。只看長度無法分辨完整的回覆和被截斷的回覆。
184
+ 2. UTF-8 安全的切點由 Lua 決定並回報,呼叫端從那裡接著讀,兩邊從不各自計算邊界。
185
+ 3. 逐檔校驗位元組數(用 `Buffer.byteLength`,不是 `String.length`)。任何一檔對不上就中止整次抽取,因為局部正確的快照看起來像成功。
186
+
187
+ 每次呼叫把資料暫存在自己的 key 底下:`shared.RBXEXTRACT_<32 位十六進位>`(只在 Lua VM 記憶體裡,不進 DataModel 也不進 `.rbxl`),用完就清掉,所以重疊的呼叫不會讀到彼此的資料。實例名稱幾乎可以是任何字串,包括 `..` 與路徑分隔符,所以每段路徑都會先清理,寫入前也會確認最終路徑落在輸出資料夾內。
188
+
189
+ 檔案一律獨佔建立,從不覆蓋任何東西。名字已經被占用時(包括檔案系統視為同名的名字,例如不分大小寫的磁碟上的 `Σ`、`ς` 與 `σ`),該腳本改用下一個 `~n` 後綴,`scene/scripts.json` 記下實際寫入的路徑。重跑到同一個資料夾時,先刪掉該資料夾上一次 `scene/scripts.json` 列出的檔案,其他一律不動;`scene/` 底下的檔案不是 Studio Sidecar 寫的資料夾會被拒絕。
190
+
191
+ **Studio 橋接必須和 server 活得一樣久。** 每次呼叫後就關閉它,會讓第一次呼叫成功、之後每次都失敗,錯誤訊息是 "Unable to reach Roblox Studio right now",聽起來像 Studio 沒開。其實不是:關閉會殺掉 stub,下一次呼叫就和 broker 搶時間。冷啟動時,新的 stub 還沒連上 broker 之前也會出現同一句訊息,所以橋接只針對這一句訊息重試列出 Studio 的呼叫,最多 20 秒;每次重試前的等待比上一次多 250 毫秒,最長 1.5 秒。server 開始關機後,橋接拒絕任何新連線,所以那時還在收尾的呼叫不會再生出一隻新的 stub。
192
+
193
+ **Open Cloud 接受的格式很窄。** Model 只接受 `.fbx`;Decal 接受 bmp、jpeg、png 與 tga;Audio 接受 mp3 與 ogg,只能新建、不能更新,而且有每月配額。格式錯誤要等輪詢 operation 之後才會失敗,所以在本機先擋下。Model 與 Decal 從未支援封存,`roblox_archive_asset` 一開始就會說明。
194
+
195
+ ## 平台支援
196
+
197
+ | | Windows | macOS |
198
+ |---|:---:|:---:|
199
+ | 開啟指定 place | ✅ 已驗證 | ✅ 已實作,**未在真機驗證** |
200
+ | 行程與 broker 查詢 | ✅ 已驗證(PowerShell) | ✅ 已驗證(`ps` + `lsof`) |
201
+ | stub 清理 | ✅ 已驗證 | ✅ 已實作,**未在真機驗證** |
202
+ | 視窗標題 | ✅ | ❌ 一律 `undefined`(需要輔助使用權限) |
203
+ | Open Cloud 工具 | ✅ | ✅(一般 HTTP) |
204
+
205
+ 平台差異隔離在 `ports/process-inspector.ts` 後面;broker 判定、stub 分類與清理都共用。Linux 會明確拒絕,而不是回傳空的快照,因為 Roblox Studio 不能在 Linux 上執行,空的回答會被讀成「沒有 Studio 在跑」。
206
+
207
+ ## 邊界
208
+
209
+ - **不改變視窗焦點。** 搶焦點可能讓一個游離按鍵關掉剛載好的 place。
210
+ - **`roblox_luau_safe` 不是執行任意程式碼的新途徑。** `execute_luau` 本來就在 client 手上;這個工具只是在同一個入口前加上強制的 PlaceId 守衛、包住整段程式的單一 `pcall` 與分塊結果。`pcall` 會接住執行期錯誤,把它變成可讀的回傳值;錯誤之後的程式不會執行,錯誤之前做的修改也不會復原。它不是沙箱。
211
+ - **不逆向 broker 在 13469 上的私有協定。** 以一般 MCP client 的身分和 `StudioMCP` 溝通,就沒有這個必要。
212
+ - **不提供「從物品欄移除」。** 那要透過 `inventory.roblox.com`,並帶登入後的 session cookie 與 CSRF token,Open Cloud API 金鑰做不到。
213
+ - **已知限制:輸出路徑上的資料夾可能在檢查與檔案操作之間被換掉。** 寫入檔案、或刪掉上一次抽取的檔案之前,每一層資料夾都會先用 `realpath` 解析並確認落在輸出資料夾內;但同一台機器上的其他程式可以在這之間把某個資料夾換成 symlink(time-of-check/time-of-use)。最後那個檔案以 `O_EXCL` 建立,所以那個名字上已經存在的檔案或 symlink 絕不會被打開;平台有 `O_NOFOLLOW` 時也會加上,而 Windows(libuv)不支援它。
214
+ - **已知限制:buffer 釋放失敗時,那把 key 會留下來。** 釋放 Luau buffer 失敗,或因為 server 正在關機而跳過時,那次呼叫的 `shared.RBXEXTRACT_…` 會留在 Studio 的 `shared` 表裡,沒有重試,也沒有過期。
215
+
216
+ ## 開發
217
+
218
+ ```bash
219
+ corepack pnpm install
220
+ corepack pnpm run verify # 型別檢查、測試、build、stdio smoke
221
+ corepack pnpm run verify:live # 對真實的 Studio 與帳號做唯讀檢查
222
+ corepack pnpm run verify:extract # 端到端:open_place → place_guard → luau_safe → extract_place
223
+ ```
224
+
225
+ `verify:live` 只做唯讀操作:不上傳、不更新、不開 place、不終止行程。它的 Open Cloud 部分需要 `ROBLOX_API_KEY` 和一個資產 ID(`node scripts/verify-live.mjs <assetId>`),缺少時就跳過。
226
+
227
+ `verify:extract` 需要 `ROBLOX_VERIFY_PLACE_ID`、`ROBLOX_VERIFY_UNIVERSE_ID`(你自己擁有的 place)與 `ROBLOX_VERIFY_BASELINE`(一份手工製作、逐位元組校驗過的腳本快照)。它會開一個 Studio 實例,把抽出來的腳本逐位元組和快照比對。這些步驟刻意串成一條,因為橋接生命週期的 bug 只有在連續多次工具呼叫時才會出現。
228
+
229
+ ## 授權與商標
230
+
231
+ [MIT](https://github.com/gjlmotea/studio-sidecar-mcp/blob/main/LICENSE)。
232
+
233
+ Studio Sidecar 是獨立的開源專案,與 Roblox Corporation 無隸屬關係,也未獲其背書。Roblox、Roblox Studio 與 Luau 是 Roblox Corporation 的商標。
@@ -0,0 +1,27 @@
1
+ import type { ProcessInspector, RawProcess } from '../ports/process-inspector.js';
2
+ /**
3
+ * macOS 的行程與連線查詢:`ps` + `lsof`。
4
+ *
5
+ * 解析邏輯全部抽成純函式並有單元測試 —— 這台開發機是 Windows,
6
+ * 本檔的整合路徑**沒有在真 macOS 上跑過**,所以至少要讓解析本身是被驗證的。
7
+ */
8
+ /** `ps -axo pid=,ppid=,lstart=,comm=` 的一行。 */
9
+ export declare function parsePsLine(line: string): RawProcess | undefined;
10
+ export declare function parsePsOutput(stdout: string): readonly RawProcess[];
11
+ /**
12
+ * `lsof -Fp` 的輸出:每筆記錄一行,pid 那行以 `p` 開頭。
13
+ * 找不到符合的連線時 lsof 回結束碼 1 且無輸出,那是正常結果不是錯誤。
14
+ */
15
+ export declare function parseLsofPids(stdout: string): readonly number[];
16
+ export declare class DarwinProcessInspector implements ProcessInspector {
17
+ readonly platform: NodeJS.Platform;
18
+ listAll(): Promise<readonly RawProcess[]>;
19
+ listeningPid(port: number): Promise<number | undefined>;
20
+ connectedPids(port: number): Promise<readonly number[]>;
21
+ isAlive(pid: number): Promise<boolean>;
22
+ kill(pids: readonly number[]): Promise<readonly {
23
+ pid: number;
24
+ reason: string;
25
+ }[]>;
26
+ }
27
+ //# sourceMappingURL=darwin-process-inspector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"darwin-process-inspector.d.ts","sourceRoot":"","sources":["../../src/adapters/darwin-process-inspector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAGlF;;;;;GAKG;AAEH,8CAA8C;AAC9C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CA6BhE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,CAKnE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAQ/D;AAED,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAY;IAExC,OAAO,IAAI,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAM9C;IAEK,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO5D;IAEK,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAO5D;IAEK,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3C;IAEK,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAUvF;CACF"}
@@ -0,0 +1,93 @@
1
+ import { run } from './shell.js';
2
+ /**
3
+ * macOS 的行程與連線查詢:`ps` + `lsof`。
4
+ *
5
+ * 解析邏輯全部抽成純函式並有單元測試 —— 這台開發機是 Windows,
6
+ * 本檔的整合路徑**沒有在真 macOS 上跑過**,所以至少要讓解析本身是被驗證的。
7
+ */
8
+ /** `ps -axo pid=,ppid=,lstart=,comm=` 的一行。 */
9
+ export function parsePsLine(line) {
10
+ // lstart 是固定五欄的日期(`Fri Aug 30 02:48:00 2026`),comm 可能含空白,
11
+ // 所以按位置切而不是無腦 split。
12
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.*)$/.exec(line);
13
+ if (!match)
14
+ return undefined;
15
+ const [, pid, ppid, lstart, command] = match;
16
+ if (pid === undefined || ppid === undefined || command === undefined)
17
+ return undefined;
18
+ // `lstart` 是本地時間且不帶時區(`Fri Aug 30 02:48:00 2026`),
19
+ // 依執行機器的時區解讀再正規化成 ISO(UTC) —— 清理器的時數門檻是拿它相減的,
20
+ // 只要前後一致就正確。
21
+ let startedAt;
22
+ if (lstart !== undefined) {
23
+ const parsed = new Date(lstart);
24
+ startedAt = Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();
25
+ }
26
+ const executablePath = command.trim();
27
+ const name = executablePath.slice(executablePath.lastIndexOf('/') + 1);
28
+ return {
29
+ pid: Number.parseInt(pid, 10),
30
+ parentPid: Number.parseInt(ppid, 10),
31
+ startedAt,
32
+ name,
33
+ executablePath: executablePath === '' ? undefined : executablePath,
34
+ // macOS 讀不到別的 App 的視窗標題(需要 Accessibility 授權)。
35
+ windowTitle: undefined,
36
+ };
37
+ }
38
+ export function parsePsOutput(stdout) {
39
+ return stdout
40
+ .split(/\r?\n/)
41
+ .map(parsePsLine)
42
+ .filter((row) => row !== undefined);
43
+ }
44
+ /**
45
+ * `lsof -Fp` 的輸出:每筆記錄一行,pid 那行以 `p` 開頭。
46
+ * 找不到符合的連線時 lsof 回結束碼 1 且無輸出,那是正常結果不是錯誤。
47
+ */
48
+ export function parseLsofPids(stdout) {
49
+ const pids = new Set();
50
+ for (const line of stdout.split(/\r?\n/)) {
51
+ if (!line.startsWith('p'))
52
+ continue;
53
+ const value = Number.parseInt(line.slice(1), 10);
54
+ if (Number.isFinite(value))
55
+ pids.add(value);
56
+ }
57
+ return [...pids];
58
+ }
59
+ export class DarwinProcessInspector {
60
+ platform = 'darwin';
61
+ async listAll() {
62
+ const { stdout } = await run('ps', ['-axo', 'pid=,ppid=,lstart=,comm='], {
63
+ allowFailure: true,
64
+ timeoutMs: 60_000,
65
+ });
66
+ return parsePsOutput(stdout);
67
+ }
68
+ async listeningPid(port) {
69
+ const { stdout } = await run('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fp'], { allowFailure: true, timeoutMs: 15_000 });
70
+ return parseLsofPids(stdout)[0];
71
+ }
72
+ async connectedPids(port) {
73
+ const { stdout } = await run('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:ESTABLISHED', '-Fp'], { allowFailure: true, timeoutMs: 15_000 });
74
+ return parseLsofPids(stdout);
75
+ }
76
+ async isAlive(pid) {
77
+ const { code } = await run('ps', ['-p', String(pid)], { allowFailure: true, timeoutMs: 15_000 });
78
+ return code === 0;
79
+ }
80
+ async kill(pids) {
81
+ const failures = [];
82
+ for (const pid of pids) {
83
+ const { code, stderr } = await run('kill', ['-9', String(pid)], {
84
+ allowFailure: true,
85
+ timeoutMs: 15_000,
86
+ });
87
+ if (code !== 0)
88
+ failures.push({ pid, reason: stderr.trim() || `kill exit code ${code}` });
89
+ }
90
+ return failures;
91
+ }
92
+ }
93
+ //# sourceMappingURL=darwin-process-inspector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"darwin-process-inspector.js","sourceRoot":"","sources":["../../src/adapters/darwin-process-inspector.ts"],"names":[],"mappings":"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"}