-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlaunch_studio_mcp.py
More file actions
160 lines (138 loc) · 6.25 KB
/
Copy pathlaunch_studio_mcp.py
File metadata and controls
160 lines (138 loc) · 6.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
157
158
159
160
# SPDX-License-Identifier: GPL-3.0-or-later
# launch_studio_mcp.py
# ──────────────────────────────────────────────────────────────────────────
# Robust launcher for Roblox's StudioMCP.exe (the Studio MCP studio server).
#
# Roblox ships a %LOCALAPPDATA%\Roblox\mcp.bat, but it hard-codes ONE Studio
# version path. When Studio auto-updates, that folder is eventually removed and
# the .bat's fallback branch is broken batch syntax (`else` on its own line),
# so StudioMCP.exe never launches -> the bridge sees 0 tools -> the extension
# reports "Bridge or Studio offline".
#
# This launcher sidesteps that entirely: it finds the NEWEST StudioMCP.exe
# across all installed Studio versions and launches it, transparently forwarding
# stdio and any CLI args. It also supports an explicit override path via
# `ZS_STUDIO_MCP_PATH` when discovery is not enough.
# ──────────────────────────────────────────────────────────────────────────
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from typing import Iterable, Optional
ENV_OVERRIDE = "ZS_STUDIO_MCP_PATH"
WINDOWS_STUDIO_EXECUTABLES = ("RobloxStudioBeta.exe", "RobloxStudio.exe")
MAC_STUDIO_EXECUTABLES = ("RobloxStudio", "RobloxStudioBeta", "Roblox")
def _candidate_roots() -> list[Path]:
"""Directories that may contain Roblox Studio version folders (Windows)."""
roots: list[Path] = []
local_appdata = os.environ.get("LOCALAPPDATA")
if local_appdata:
roots.append(Path(local_appdata) / "Roblox" / "Versions") # ← official
roots.append(Path(local_appdata) / "Bloxstrap" / "Versions") # ← bloxstrap
roots.append(Path(local_appdata) / "Fishstrap" / "Versions") # ← fishstrap
for env in ("ProgramFiles", "ProgramFiles(x86)"):
value = os.environ.get(env)
if value:
roots.append(Path(value) / "Fishstrap" / "Versions")
roots.append(Path(value) / "Bloxstrap" / "Versions")
return roots
def _resolve_override_path(path_value: str) -> Optional[Path]:
path = Path(path_value).expanduser()
if path.is_file():
return path
if path.is_dir():
if sys.platform == "darwin":
candidate = path / "Contents" / "MacOS" / "StudioMCP"
else:
candidate = path / "StudioMCP.exe"
if candidate.is_file():
return candidate
return None
def _newest_path(paths: Iterable[Path]) -> Optional[Path]:
try:
return max(paths, key=lambda p: p.stat().st_mtime)
except (ValueError, OSError):
return None
def _find_studio_mcp_windows() -> Optional[Path]:
"""Return the path to the StudioMCP.exe of the live Studio install, or None.
Roblox leaves "zombie" version folders behind after an update: they still
contain a StudioMCP.exe but no RobloxStudioBeta.exe / RobloxStudio.exe
(the actual Studio is gone). Picking the newest StudioMCP.exe by mtime can
land on such a zombie, which launches fine but has no Studio to attach to ->
the bridge sees 0 tools.
We only consider version folders that ALSO contain a current Studio
executable, and prefer the newest of those. We keep zombie StudioMCP.exe
paths only as a last-resort fallback if no paired install exists.
"""
paired: list[Path] = []
orphans: list[Path] = []
for root in _candidate_roots():
if not root.is_dir():
continue
try:
for version_dir in root.iterdir():
if not version_dir.is_dir():
continue
studio_mcp = version_dir / "StudioMCP.exe"
if not studio_mcp.is_file():
continue
if any((version_dir / exe_name).is_file() for exe_name in WINDOWS_STUDIO_EXECUTABLES):
paired.append(studio_mcp)
else:
orphans.append(studio_mcp)
except OSError:
continue
return _newest_path(paired) or _newest_path(orphans)
def _mac_app_candidates() -> list[Path]:
"""Locations where Roblox Studio may be installed on macOS."""
home = Path.home()
return [
Path("/Applications/RobloxStudio.app"),
home / "Applications" / "RobloxStudio.app",
Path("/Applications/Roblox.app"),
home / "Applications" / "Roblox.app",
Path("/Applications/RobloxStudioBeta.app"),
home / "Applications" / "RobloxStudioBeta.app",
]
def _find_studio_mcp_mac() -> Optional[Path]:
"""Return the path to StudioMCP inside a Roblox Studio app bundle, or None."""
for app in _mac_app_candidates():
macos_dir = app / "Contents" / "MacOS"
studio_mcp = macos_dir / "StudioMCP"
if not studio_mcp.is_file():
continue
if any((macos_dir / exe_name).is_file() for exe_name in MAC_STUDIO_EXECUTABLES):
return studio_mcp
return None
def find_studio_mcp() -> Optional[Path]:
override_value = os.environ.get(ENV_OVERRIDE)
if override_value:
override_path = _resolve_override_path(override_value)
if override_path:
return override_path
sys.stderr.write(
f"launch_studio_mcp: {ENV_OVERRIDE} is set but does not point to a valid StudioMCP binary: {override_value}\n"
)
if sys.platform == "darwin":
return _find_studio_mcp_mac()
return _find_studio_mcp_windows()
def main() -> int:
exe = find_studio_mcp()
binary_name = "StudioMCP" if sys.platform == "darwin" else "StudioMCP.exe"
if not exe:
sys.stderr.write(
f"launch_studio_mcp: no {binary_name} found. Open Roblox Studio and "
"enable 'Studio as MCP server' (Assistant Settings > MCP Servers).\n"
)
return 1
sys.stderr.write(f"launch_studio_mcp: using {exe}\n")
sys.stderr.flush()
proc = subprocess.Popen([str(exe)] + sys.argv[1:])
try:
return proc.wait()
except KeyboardInterrupt:
proc.terminate()
return proc.wait()
if __name__ == "__main__":
sys.exit(main())