ZeroScript lets AI chat sites (DeepSeek, Kimi, ChatGPT, …) control Bllox Studio through MCP. Three runtime parts, no build step:
bridge.py— Python WebSocket server onws://127.0.0.1:17613. Spawns each MCP server declared inconfig.jsonas a stdio child, aggregates their tools, routes by tool name. Only dependency:websockets(Python 3.9+ required —asyncio.to_thread).launch_studio_mcp.py— finds the NEWESTStudioMCP.exeunder%LOCALAPPDATA%\Roblox\Versions(plus Bloxstrap/Fishstrap), preferring folders that still containRobloxStudioBeta.exe. Override: envZS_STUDIO_MCP_PATH.zeroscript-extension/— Chrome MV3 extension, loaded unpacked. No build, no bundler.
Ports: 17613 bridge <-> extension; 13469 Roblox Studio's built-in MCP port. A "0 tools" symptom is usually a port conflict (leftover bridge, zombie StudioMCP.exe, or a third-party squatter like ropilot) — see the heavily-commented process-recovery code in bridge.py before touching it.
Source files carry SPDX-License-Identifier: GPL-3.0-or-later. The LICENSE file and README.md say MIT. This discrepancy is unresolved — do NOT "fix" one side without being asked, since the intent is ambiguous.
providers/*.js each expose a global ZSProvider IIFE; the content script loads them in fixed order: core/config.py, core/parser.js, providers/<site>.js, core/main.js with overlay.css. core/main.js is provider-agnostic and must NEVER touch the host site's DOM — all site access goes through P (the provider interface). To add a site you must touch:
manifest.json— content_scripts entry (4 JS + css above) and matchinghost_permissions.background.js— add the URL pattern to bothPROVIDER_URLSandKNOWN_EXCLUDE.core/main.js—AI_SITESentry; itsnamemust equal the provider'sdisplayName.- The new file
providers/<site>.js— mirror an existing provider (kimi.js is the reference one).
Grep for "Keep in sync" — the codebase flags other mirrored lists the same way.
- Every file starts with
// SPDX-License-Identifier: GPL-3.0-or-later(Python:#). Keep it. bridge.py'sBRIDGE_VERSION(line 86) is meant to trackmanifest.json"version" (printed at startup so a terminal screenshot identifies the build). Bump both together.- The terminal is the diagnostic surface for non-technical users: console output stays strictly ASCII, noisy detail goes through
log(..., terminal=False)(onlylogs/bridge_debug.log, which is append-only), user action steps go throughaction_banner(). Don't add glyphs/emoji to console output. - Primary server protection:
PRIMARY_SERVER_ID = "roblox"inbridge.py:144is hardcoded.config_json_add_server/config_remove_server(and the extension's add/remove_server messages) refuse to touch it. Add-ons get rewritten into config.json and the bridge restarts itself. Note: the currentconfig.jsonuses keyroblox-mcp— the protection targets the constant"roblox", not the config key. - Windows process handling is deliberately careful: only kills processes it can PROVE are leftovers (cmdline/process-tree checks, port ownership via
netstat -anobothTCPandTCPv6), usestaskkill /F /Tfor trees. Preserve the "never kill on suspicion" guard if you edit it. logs/*.logare gitignored; never commit them..gitattributespins LF for*.sh/*.command(CRLF breaks the macOS launcher) — keep that.
release.yml— unified release. Triggered on push to main that changeszeroscript-extension/manifest.json, or manually (workflow_dispatch). Creates the GitHub Release with the extension zip attached, then builds the Tauri desktop GUI on Win/macOS/Linux and attaches those installers to the same release.build-desktop-gui.yml— standalone Tauri desktop build, triggered onrelease: publishedor manually. Superseded byrelease.yml's desktop job but kept for manual rebuilds.delete.yml— housekeeping, deletes old workflow runs on a daily schedule.
There is no separate build-desktop.yml or build-extension.yml — those were merged into release.yml.
packaging/build.pybuilds PyInstaller bridge binaries (--binaries-onlyflag produces sidecars only). Full desktop packages (Windows NSIS, macOS DMG, Linux AppImage) are built inside therelease.yml/build-desktop-gui.ymlworkflows, not bybuild.pydirectly.- bridge.py is frozen-aware:
config.json+logs/live next to the executable (~/.zeroscripton Linux AppImage, which is a read-only mount), a bare.pycommand in config.json maps to the bundled sibling executable (_sibling_exe),restart_self()and_reclaim_bridge_port()handle the packaged executable, and macOS windowed builds get devnull std streams. - If a configured Roblox MCP server needs Node.js (
npx/npm/nodein its command) and none is installed, the bridge prints an ACTION NEEDED banner and falls back to Roblox Studio's built-in StudioMCP vialaunch_studio_mcp(_needs_node_fallback/_apply_node_fallback, once per process). - Keep
BRIDGE_VERSION,manifest.json"version", andCHANGELOG.mdin sync when packaging; the build workflow passes the release tag as--version.
zeroscript-desktop/is a Tauri v2 app (Vite + vanilla TypeScript) that runs the PyInstaller bridge as a hidden sidecar (bundle.externalBin->src-tauri/binaries, named with the Rust host target-triple suffix)._sibling_exein bridge.py globs for those suffixed names.config.json+logs/go to Tauri's per-OS app-data dir via theZS_DATA_DIRenv set when spawning the sidecar.src-tauri/src/lib.rsowns start/stop/restart, streams sidecar output to the GUI (bridge-log/bridge-exitevents), tray + minimize-to-tray, single-instance guard, and the autostart plugin. The GUI talks to the bridge over the SAMEws://127.0.0.1:17613protocol as the extension (src/bridge-ws.ts); the browser extension remains the AI-chat UI.- Local dev:
npm run prepare:sidecars:stub(placeholders — runpython bridge.pyin a terminal for a real bridge) or real sidecars viapackaging/build.py --binaries-only+npm run prepare:sidecars, thennpm run tauri dev.
- Parser tests (the only automated tests in the repo):
node test-parser.jsfromzeroscript-extension/(expectsnode; usesrequire, so no bundler). - Run the bridge during dev (Windows):
pip install websockets, thenpython bridge.pyfrom the repo root.start.bat/MacOS_Start.commandare the user-facing launchers (Python 3.9+, frees port 17613 from a previous bridge, logs tologs/start.log).ZS_BRIDGE_PORToverrides the port — sync it withbackground.jsPORTif you choose another. - Extension ↔ bridge protocol (bridges in
bridge.pyhandle inhandler()):ping/pong,studio_status,list_tools,call_tool,add_server/remove_server,restart_mcp.background.jsowns the single reconnecting socket and resolves every request even offline — keep that contract when changing either side.
- No CI, no linters, no typecheck config. Unit tests are only the parser smoke tests.
- The command format the model emits is defined in
core/config.jssystem prompt + parsed incore/parser.js(pure string logic, no DOM):###LUA### … ###END_LUA###(optional:Edit|:Client|:Server, defaults Edit) or{"command": …, "params": {…}}.###LUA###always maps toexecute_luau. Parser changes MUST stay compatible with the existing tests and the system prompt text in config.js. - Provider files carry "last validated: — " notes; site DOM changes break them (that's the norm, not a bug).