@chrrxs/robloxstudio-mcp 2.17.1 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -188,6 +188,14 @@ var init_bridge_service = __esm({
188
188
  }
189
189
  }
190
190
  }
191
+ unregisterInstanceId(instanceId) {
192
+ const matching = this.matchingInstancesForInstanceId(instanceId);
193
+ const removed = matching.map(toPublic);
194
+ for (const inst of matching) {
195
+ this.unregisterInstance(inst.pluginSessionId);
196
+ }
197
+ return removed;
198
+ }
191
199
  getInstances() {
192
200
  return Array.from(this.instances.values());
193
201
  }
@@ -889,8 +897,11 @@ var init_http_server = __esm({
889
897
  get_device_simulator_state: (tools, body) => tools.getDeviceSimulatorState(body.target, body.deviceId, body.includeDeviceList, body.instance_id),
890
898
  set_device_simulator: (tools, body) => tools.setDeviceSimulator(body.target, body.deviceId, body.orientation, body.resolution, body.pixelDensity, body.scalingMode, body.stopSimulation, body.instance_id),
891
899
  capture_device_matrix: (tools, body) => tools.captureDeviceMatrix(body.entries, body.target, body.format, body.quality, body.settleSeconds, body.restoreAfter, body.instance_id),
900
+ manage_instance: (tools, body) => tools.manageInstance(body),
901
+ solo_playtest: (tools, body) => tools.soloPlaytest(body.action, body.mode, body.timeout, body.instance_id),
892
902
  start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers, body.instance_id),
893
903
  stop_playtest: (tools, body) => tools.stopPlaytest(body.instance_id),
904
+ multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id),
894
905
  multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id),
895
906
  multiplayer_test_state: (tools, body) => tools.multiplayerTestState(body.instance_id),
896
907
  multiplayer_test_add_players: (tools, body) => tools.multiplayerTestAddPlayers(body.numPlayers, body.timeout, body.instance_id),
@@ -1469,6 +1480,14 @@ var init_opencloud_client = __esm({
1469
1480
  async getAssetDetails(assetId) {
1470
1481
  return this.request(`/toolbox-service/v2/assets/${assetId}`);
1471
1482
  }
1483
+ async listAssetVersions(assetId, maxPageSize = 10, pageToken) {
1484
+ return this.request(`/assets/v1/assets/${assetId}/versions`, {
1485
+ params: {
1486
+ maxPageSize,
1487
+ pageToken
1488
+ }
1489
+ });
1490
+ }
1472
1491
  async getAssetThumbnail(assetId, size = "420x420") {
1473
1492
  const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${assetId}&size=${size}&format=Png`;
1474
1493
  try {
@@ -1697,6 +1716,254 @@ var init_roblox_cookie_client = __esm({
1697
1716
  }
1698
1717
  });
1699
1718
 
1719
+ // ../core/dist/studio-instance-manager.js
1720
+ import { execFileSync, spawn } from "child_process";
1721
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
1722
+ import * as os from "os";
1723
+ import * as path from "path";
1724
+ function run(command, args, options = {}) {
1725
+ return execFileSync(command, args, {
1726
+ encoding: "utf8",
1727
+ stdio: ["ignore", "pipe", "pipe"],
1728
+ ...options
1729
+ }).trim();
1730
+ }
1731
+ function isWsl() {
1732
+ if (process.platform !== "linux")
1733
+ return false;
1734
+ try {
1735
+ return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf8"));
1736
+ } catch {
1737
+ return false;
1738
+ }
1739
+ }
1740
+ function powershell(script) {
1741
+ return run("powershell.exe", ["-NoProfile", "-Command", script], {
1742
+ cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
1743
+ });
1744
+ }
1745
+ function windowsLocalAppData() {
1746
+ if (process.platform === "win32")
1747
+ return process.env.LOCALAPPDATA;
1748
+ if (!isWsl())
1749
+ return void 0;
1750
+ try {
1751
+ return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
1752
+ cwd: existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
1753
+ });
1754
+ } catch {
1755
+ return void 0;
1756
+ }
1757
+ }
1758
+ function toWslPath(windowsPath) {
1759
+ if (!isWsl())
1760
+ return windowsPath;
1761
+ return run("wslpath", ["-u", windowsPath]);
1762
+ }
1763
+ function toStudioLaunchArg(arg) {
1764
+ if (!isWsl() || !path.isAbsolute(arg) || !existsSync(arg))
1765
+ return arg;
1766
+ return run("wslpath", ["-w", arg]);
1767
+ }
1768
+ function resolveStudioExe() {
1769
+ if (process.env.ROBLOX_STUDIO_EXE)
1770
+ return process.env.ROBLOX_STUDIO_EXE;
1771
+ if (process.platform === "darwin") {
1772
+ return "/Applications/RobloxStudio.app/Contents/MacOS/RobloxStudio";
1773
+ }
1774
+ if (process.platform !== "win32" && !isWsl()) {
1775
+ throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
1776
+ }
1777
+ const localAppData = windowsLocalAppData();
1778
+ const root = localAppData ? path.join(toWslPath(localAppData), "Roblox", "Versions") : path.join(os.homedir(), "AppData", "Local", "Roblox", "Versions");
1779
+ if (!existsSync(root)) {
1780
+ throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
1781
+ }
1782
+ const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
1783
+ if (candidates.length === 0) {
1784
+ throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
1785
+ }
1786
+ return candidates[0];
1787
+ }
1788
+ function listStudioProcesses() {
1789
+ if (process.platform === "darwin") {
1790
+ let out2 = "";
1791
+ try {
1792
+ out2 = run("pgrep", ["-fl", "RobloxStudio"]);
1793
+ } catch {
1794
+ return [];
1795
+ }
1796
+ return out2.split("\n").filter(Boolean).map((line) => {
1797
+ const [pid, ...rest] = line.trim().split(/\s+/);
1798
+ return { Id: Number(pid), Path: rest.join(" "), MainWindowTitle: "" };
1799
+ }).filter((proc) => Number.isFinite(proc.Id));
1800
+ }
1801
+ if (process.platform !== "win32" && !isWsl())
1802
+ return [];
1803
+ let out = "";
1804
+ try {
1805
+ out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | Select-Object Id,Path,MainWindowTitle | ConvertTo-Json -Compress");
1806
+ } catch {
1807
+ return [];
1808
+ }
1809
+ if (!out)
1810
+ return [];
1811
+ const parsed = JSON.parse(out);
1812
+ return Array.isArray(parsed) ? parsed : [parsed];
1813
+ }
1814
+ function buildStudioLaunchArgs(options) {
1815
+ switch (options.source) {
1816
+ case "baseplate":
1817
+ return [];
1818
+ case "local_file":
1819
+ if (!options.localPlaceFile)
1820
+ throw new Error('local_place_file is required when source="local_file".');
1821
+ return ["--task", "EditFile", "--localPlaceFile", options.localPlaceFile];
1822
+ case "published_place":
1823
+ if (!options.placeId)
1824
+ throw new Error('place_id is required when source="published_place".');
1825
+ if (!options.universeId)
1826
+ throw new Error('universe_id is required when source="published_place".');
1827
+ return ["--task", "EditPlace", "--placeId", String(options.placeId), "--universeId", String(options.universeId)];
1828
+ case "place_revision":
1829
+ if (!options.placeId)
1830
+ throw new Error('place_id is required when source="place_revision".');
1831
+ if (!options.universeId)
1832
+ throw new Error('universe_id is required when source="place_revision".');
1833
+ if (!options.placeVersion)
1834
+ throw new Error('place_version is required when launching source="place_revision".');
1835
+ return [
1836
+ "--task",
1837
+ "EditPlaceRevision",
1838
+ "--placeId",
1839
+ String(options.placeId),
1840
+ "--universeId",
1841
+ String(options.universeId),
1842
+ "--placeVersion",
1843
+ String(options.placeVersion)
1844
+ ];
1845
+ }
1846
+ }
1847
+ function delay(ms) {
1848
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1849
+ }
1850
+ var StudioInstanceManager;
1851
+ var init_studio_instance_manager = __esm({
1852
+ "../core/dist/studio-instance-manager.js"() {
1853
+ "use strict";
1854
+ StudioInstanceManager = class {
1855
+ managedByInstanceId = /* @__PURE__ */ new Map();
1856
+ pending = /* @__PURE__ */ new Set();
1857
+ list() {
1858
+ return [...this.managedByInstanceId.values(), ...this.pending].filter((instance, index, all) => all.indexOf(instance) === index);
1859
+ }
1860
+ get(instanceId) {
1861
+ return this.managedByInstanceId.get(instanceId);
1862
+ }
1863
+ attachInstanceId(record, instanceId) {
1864
+ record.instanceId = instanceId;
1865
+ this.pending.delete(record);
1866
+ this.managedByInstanceId.set(instanceId, record);
1867
+ }
1868
+ async launch(options) {
1869
+ const before = new Set(listStudioProcesses().map((proc2) => proc2.Id));
1870
+ const exe = resolveStudioExe();
1871
+ const args = buildStudioLaunchArgs(options).map(toStudioLaunchArg);
1872
+ const proc = spawn(exe, args, {
1873
+ cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
1874
+ detached: true,
1875
+ stdio: "ignore"
1876
+ });
1877
+ proc.unref();
1878
+ const record = {
1879
+ source: options.source,
1880
+ spawnPid: proc.pid,
1881
+ exe,
1882
+ args,
1883
+ placeId: options.placeId,
1884
+ universeId: options.universeId,
1885
+ placeVersion: options.placeVersion,
1886
+ localPlaceFile: options.localPlaceFile,
1887
+ launchedAt: Date.now()
1888
+ };
1889
+ this.pending.add(record);
1890
+ const deadline = Date.now() + 5e3;
1891
+ while (Date.now() < deadline && record.nativeProcessId === void 0) {
1892
+ const created = listStudioProcesses().find((candidate) => !before.has(candidate.Id));
1893
+ if (created) {
1894
+ record.nativeProcessId = created.Id;
1895
+ break;
1896
+ }
1897
+ await delay(250);
1898
+ }
1899
+ if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
1900
+ record.nativeProcessId = proc.pid;
1901
+ }
1902
+ return record;
1903
+ }
1904
+ close(record) {
1905
+ const processId = record.nativeProcessId ?? record.spawnPid;
1906
+ if (!processId) {
1907
+ throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
1908
+ }
1909
+ if (process.platform === "win32" || isWsl()) {
1910
+ powershell(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
1911
+ } else {
1912
+ try {
1913
+ process.kill(processId, "SIGTERM");
1914
+ } catch (error) {
1915
+ if (error.code !== "ESRCH")
1916
+ throw error;
1917
+ }
1918
+ }
1919
+ record.closedAt = Date.now();
1920
+ if (record.instanceId)
1921
+ this.managedByInstanceId.delete(record.instanceId);
1922
+ this.pending.delete(record);
1923
+ }
1924
+ closeConnectedInstance(instance) {
1925
+ const process2 = this.findProcessForConnectedInstance(instance);
1926
+ if (!process2) {
1927
+ throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
1928
+ }
1929
+ this.closeProcess(process2.Id);
1930
+ }
1931
+ closeProcess(processId) {
1932
+ if (process.platform === "win32" || isWsl()) {
1933
+ powershell(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
1934
+ } else {
1935
+ try {
1936
+ process.kill(processId, "SIGTERM");
1937
+ } catch (error) {
1938
+ if (error.code !== "ESRCH")
1939
+ throw error;
1940
+ }
1941
+ }
1942
+ }
1943
+ findProcessForConnectedInstance(instance) {
1944
+ const processes = listStudioProcesses();
1945
+ if (processes.length === 0)
1946
+ return void 0;
1947
+ if (processes.length === 1)
1948
+ return processes[0];
1949
+ const names = [instance.dataModelName, instance.placeName].map((name) => name.trim()).filter((name, index, all) => name.length > 0 && all.indexOf(name) === index);
1950
+ const candidates = processes.filter((proc) => {
1951
+ const title = (proc.MainWindowTitle ?? "").trim();
1952
+ if (!title)
1953
+ return false;
1954
+ return names.some((name) => title === `${name} - Roblox Studio` || title.startsWith(`${name} - `) || title.startsWith(`${name} (`));
1955
+ });
1956
+ if (candidates.length === 1)
1957
+ return candidates[0];
1958
+ if (candidates.length > 1) {
1959
+ throw new Error(`Multiple Studio processes matched connected instance "${instance.instanceId}".`);
1960
+ }
1961
+ return void 0;
1962
+ }
1963
+ };
1964
+ }
1965
+ });
1966
+
1700
1967
  // ../core/dist/jpeg-encoder.js
1701
1968
  function rgbaToJpeg(rgba, width, height, quality = 80) {
1702
1969
  if (width <= 0 || height <= 0)
@@ -2715,8 +2982,8 @@ var init_png_encoder = __esm({
2715
2982
 
2716
2983
  // ../core/dist/tools/index.js
2717
2984
  import * as fs from "fs";
2718
- import * as os from "os";
2719
- import * as path from "path";
2985
+ import * as os2 from "os";
2986
+ import * as path2 from "path";
2720
2987
  function encodeImageFromRgbaResponse(response, format, quality) {
2721
2988
  if (!response.data || response.width === void 0 || response.height === void 0) {
2722
2989
  throw new Error("Render response missing data, width, or height");
@@ -3072,6 +3339,7 @@ var init_tools = __esm({
3072
3339
  init_build_executor();
3073
3340
  init_opencloud_client();
3074
3341
  init_roblox_cookie_client();
3342
+ init_studio_instance_manager();
3075
3343
  init_jpeg_encoder();
3076
3344
  init_png_encoder();
3077
3345
  MAX_INLINE_IMAGE_BYTES = 6e6;
@@ -3129,11 +3397,34 @@ var init_tools = __esm({
3129
3397
  bridge;
3130
3398
  openCloudClient;
3131
3399
  cookieClient;
3400
+ instanceManager;
3132
3401
  constructor(bridge) {
3133
3402
  this.client = new StudioHttpClient(bridge);
3134
3403
  this.bridge = bridge;
3135
3404
  this.openCloudClient = new OpenCloudClient();
3136
3405
  this.cookieClient = new RobloxCookieClient();
3406
+ this.instanceManager = new StudioInstanceManager();
3407
+ }
3408
+ _textResult(body) {
3409
+ return { content: [{ type: "text", text: JSON.stringify(body) }] };
3410
+ }
3411
+ _parseTextResult(result) {
3412
+ const text = result?.content?.[0]?.text;
3413
+ if (typeof text !== "string")
3414
+ return {};
3415
+ try {
3416
+ const parsed = JSON.parse(text);
3417
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3418
+ } catch {
3419
+ return {};
3420
+ }
3421
+ }
3422
+ _briefRoles(instanceId, equivalentInstances = false) {
3423
+ const roles = equivalentInstances ? this._rolesForEquivalentInstances(instanceId) : this._rolesForInstance(instanceId);
3424
+ return {
3425
+ roles,
3426
+ runtimeRoles: roles.filter((role) => role === "server" || /^client-\d+$/.test(role))
3427
+ };
3137
3428
  }
3138
3429
  // Resolve (instance_id, target-role) → concrete (instanceId, role) and
3139
3430
  // dispatch a single request. Throws RoutingFailure if the resolution is
@@ -3458,8 +3749,8 @@ var init_tools = __esm({
3458
3749
  timedOut: true
3459
3750
  };
3460
3751
  }
3461
- async getFileTree(path2 = "", instance_id) {
3462
- const response = await this._callSingle("/api/file-tree", { path: path2 }, void 0, instance_id);
3752
+ async getFileTree(path3 = "", instance_id) {
3753
+ const response = await this._callSingle("/api/file-tree", { path: path3 }, void 0, instance_id);
3463
3754
  return {
3464
3755
  content: [
3465
3756
  {
@@ -3576,9 +3867,9 @@ var init_tools = __esm({
3576
3867
  ]
3577
3868
  };
3578
3869
  }
3579
- async getProjectStructure(path2, maxDepth, scriptsOnly, instance_id) {
3870
+ async getProjectStructure(path3, maxDepth, scriptsOnly, instance_id) {
3580
3871
  const response = await this._callSingle("/api/project-structure", {
3581
- path: path2,
3872
+ path: path3,
3582
3873
  maxDepth,
3583
3874
  scriptsOnly
3584
3875
  }, void 0, instance_id);
@@ -4499,8 +4790,8 @@ ${code}`
4499
4790
  const rawJson = mutable.raw_json;
4500
4791
  if (typeof rawJson === "string") {
4501
4792
  if (typeof outputPath === "string" && outputPath !== "") {
4502
- const resolvedOutputPath = path.resolve(outputPath);
4503
- fs.mkdirSync(path.dirname(resolvedOutputPath), { recursive: true });
4793
+ const resolvedOutputPath = path2.resolve(outputPath);
4794
+ fs.mkdirSync(path2.dirname(resolvedOutputPath), { recursive: true });
4504
4795
  fs.writeFileSync(resolvedOutputPath, rawJson, "utf8");
4505
4796
  mutable.output_path = resolvedOutputPath;
4506
4797
  }
@@ -4536,12 +4827,272 @@ ${code}`
4536
4827
  const body = response !== null && typeof response === "object" && !Array.isArray(response) ? { ...response, target: resolved.targetRole } : response;
4537
4828
  return { content: [{ type: "text", text: JSON.stringify(body) }] };
4538
4829
  }
4539
- async startPlaytest(mode, numPlayers, instance_id) {
4830
+ _positiveInteger(value, name) {
4831
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
4832
+ throw new Error(`${name} must be a positive number.`);
4833
+ }
4834
+ return Math.trunc(value);
4835
+ }
4836
+ _optionalPositiveInteger(value, name) {
4837
+ if (value === void 0 || value === null)
4838
+ return void 0;
4839
+ return this._positiveInteger(value, name);
4840
+ }
4841
+ _publicInstanceKey(instance) {
4842
+ return `${instance.instanceId}:${instance.role}:${instance.connectedAt}`;
4843
+ }
4844
+ _isLatestPublishedPlaceOpen(placeId) {
4845
+ const publishedInstanceId2 = `place:${placeId}`;
4846
+ return this.bridge.getPublicInstances().some((instance) => instance.placeId === placeId || instance.instanceId === publishedInstanceId2) || this.instanceManager.list().some((record) => record.closedAt === void 0 && record.source === "published_place" && record.placeId === placeId);
4847
+ }
4848
+ async _deriveUniverseId(placeId) {
4849
+ const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
4850
+ if (!response.ok) {
4851
+ const body = await response.text().catch(() => "");
4852
+ throw new Error(`Could not derive universe_id for place ${placeId} (${response.status}): ${body}`);
4853
+ }
4854
+ const data = await response.json();
4855
+ if (typeof data.universeId !== "number" || !Number.isFinite(data.universeId)) {
4856
+ throw new Error(`Could not derive universe_id for place ${placeId}.`);
4857
+ }
4858
+ return Math.trunc(data.universeId);
4859
+ }
4860
+ async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
4861
+ const deadline = Date.now() + timeoutMs;
4862
+ while (Date.now() < deadline) {
4863
+ const candidates = this.bridge.getPublicInstances().filter((instance) => instance.role === "edit").filter((instance) => !beforeKeys.has(this._publicInstanceKey(instance))).filter((instance) => instance.connectedAt >= record.launchedAt - 1e3).filter((instance) => {
4864
+ if (record.source === "published_place") {
4865
+ return record.placeId !== void 0 && instance.placeId === record.placeId;
4866
+ }
4867
+ return true;
4868
+ }).sort((a, b) => b.connectedAt - a.connectedAt);
4869
+ if (candidates[0])
4870
+ return candidates[0];
4871
+ await sleep(500);
4872
+ }
4873
+ return void 0;
4874
+ }
4875
+ _managedStatus(record) {
4876
+ const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
4877
+ return {
4878
+ instance_id: record.instanceId,
4879
+ source: record.source,
4880
+ place_id: record.placeId,
4881
+ place_version: record.placeVersion,
4882
+ connected: connected.length > 0,
4883
+ roles: connected.map((instance) => instance.role).sort()
4884
+ };
4885
+ }
4886
+ _versionNumberFromPath(pathValue) {
4887
+ const match = pathValue.match(/\/versions\/(\d+)$/);
4888
+ return match ? Number(match[1]) : void 0;
4889
+ }
4890
+ async manageInstance(request) {
4891
+ const action = request.action;
4892
+ const instance_id = typeof request.instance_id === "string" ? request.instance_id : void 0;
4893
+ if (action !== "launch" && action !== "close" && action !== "status" && action !== "list_place_versions") {
4894
+ throw new Error("manage_instance requires action=launch|close|status|list_place_versions");
4895
+ }
4896
+ if (action === "list_place_versions") {
4897
+ if (!this.openCloudClient.hasApiKey()) {
4898
+ return this._textResult({
4899
+ error: "ROBLOX_OPEN_CLOUD_API_KEY is required to list place versions."
4900
+ });
4901
+ }
4902
+ const placeId2 = this._positiveInteger(request.place_id, "place_id");
4903
+ const rawMaxPageSize = this._optionalPositiveInteger(request.max_page_size, "max_page_size") ?? 10;
4904
+ const maxPageSize = Math.max(1, Math.min(50, rawMaxPageSize));
4905
+ const pageToken = typeof request.page_token === "string" ? request.page_token : void 0;
4906
+ const response = await this.openCloudClient.listAssetVersions(placeId2, maxPageSize, pageToken);
4907
+ const body = {
4908
+ versions: (response.assetVersions ?? []).map((version) => ({
4909
+ version: this._versionNumberFromPath(version.path),
4910
+ created_at: version.createTime,
4911
+ path: version.path,
4912
+ moderation_state: version.moderationResult?.moderationState
4913
+ }))
4914
+ };
4915
+ if (response.nextPageToken)
4916
+ body.next_page_token = response.nextPageToken;
4917
+ return this._textResult(body);
4918
+ }
4919
+ if (action === "status") {
4920
+ if (instance_id) {
4921
+ const record2 = this.instanceManager.get(instance_id);
4922
+ const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
4923
+ if (!record2 && connected2.length === 0) {
4924
+ return this._textResult({ error: "Instance is not connected or managed.", instance_id });
4925
+ }
4926
+ return this._textResult({
4927
+ instance_id,
4928
+ managed: !!record2,
4929
+ source: record2?.source,
4930
+ place_id: record2?.placeId ?? connected2[0]?.placeId,
4931
+ place_version: record2?.placeVersion,
4932
+ roles: connected2.map((instance) => instance.role).sort()
4933
+ });
4934
+ }
4935
+ return this._textResult({
4936
+ managed: this.instanceManager.list().filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
4937
+ connected: this.bridge.getPublicInstances().map((instance) => ({
4938
+ instance_id: instance.instanceId,
4939
+ role: instance.role,
4940
+ place_id: instance.placeId,
4941
+ place_name: instance.placeName
4942
+ }))
4943
+ });
4944
+ }
4945
+ if (action === "close") {
4946
+ let record2;
4947
+ if (instance_id) {
4948
+ record2 = this.instanceManager.get(instance_id);
4949
+ if (!record2) {
4950
+ const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
4951
+ const edit = connected2.find((instance) => instance.role === "edit");
4952
+ if (!edit) {
4953
+ return this._textResult({
4954
+ error: "Instance is not connected or managed.",
4955
+ instance_id
4956
+ });
4957
+ }
4958
+ try {
4959
+ this.instanceManager.closeConnectedInstance(edit);
4960
+ } catch (error) {
4961
+ return this._textResult({
4962
+ error: error instanceof Error ? error.message : String(error),
4963
+ instance_id
4964
+ });
4965
+ }
4966
+ this.bridge.unregisterInstanceId(instance_id);
4967
+ return this._textResult({
4968
+ instance_id,
4969
+ message: "Studio instance closed."
4970
+ });
4971
+ }
4972
+ } else {
4973
+ const active = this.instanceManager.list().filter((entry) => entry.closedAt === void 0);
4974
+ if (active.length === 0) {
4975
+ return this._textResult({ message: "No managed Studio instances are active." });
4976
+ }
4977
+ if (active.length > 1) {
4978
+ return this._textResult({
4979
+ error: "instance_id is required because multiple managed Studio instances are active.",
4980
+ managed: active.map((entry) => this._managedStatus(entry))
4981
+ });
4982
+ }
4983
+ record2 = active[0];
4984
+ }
4985
+ this.instanceManager.close(record2);
4986
+ if (record2.instanceId)
4987
+ this.bridge.unregisterInstanceId(record2.instanceId);
4988
+ return this._textResult({
4989
+ instance_id: record2.instanceId,
4990
+ message: "Studio instance closed."
4991
+ });
4992
+ }
4993
+ const source = request.source;
4994
+ if (source !== "baseplate" && source !== "local_file" && source !== "published_place" && source !== "place_revision") {
4995
+ throw new Error("manage_instance action=launch requires source=baseplate|local_file|published_place|place_revision");
4996
+ }
4997
+ const launchSource = source;
4998
+ const placeId = launchSource === "published_place" || launchSource === "place_revision" ? this._positiveInteger(request.place_id, "place_id") : this._optionalPositiveInteger(request.place_id, "place_id");
4999
+ const placeVersion = launchSource === "place_revision" ? this._positiveInteger(request.place_version, "place_version") : this._optionalPositiveInteger(request.place_version, "place_version");
5000
+ const localPlaceFile = typeof request.local_place_file === "string" ? request.local_place_file : void 0;
5001
+ if (launchSource === "published_place" && placeId !== void 0 && this._isLatestPublishedPlaceOpen(placeId)) {
5002
+ return this._textResult({
5003
+ error: "Place is already open.",
5004
+ message: `place_id ${placeId} is already connected. Use the existing instance or launch a specific place_revision.`
5005
+ });
5006
+ }
5007
+ const universeId = launchSource === "published_place" || launchSource === "place_revision" ? this._optionalPositiveInteger(request.universe_id, "universe_id") ?? await this._deriveUniverseId(placeId) : this._optionalPositiveInteger(request.universe_id, "universe_id");
5008
+ const waitForConnection = request.wait_for_connection !== false;
5009
+ const timeoutMs = this._optionalPositiveInteger(request.timeout_ms, "timeout_ms") ?? 12e4;
5010
+ const beforeKeys = new Set(this.bridge.getPublicInstances().map((instance) => this._publicInstanceKey(instance)));
5011
+ const record = await this.instanceManager.launch({
5012
+ source: launchSource,
5013
+ localPlaceFile,
5014
+ placeId,
5015
+ universeId,
5016
+ placeVersion
5017
+ });
5018
+ if (!waitForConnection) {
5019
+ return this._textResult({ message: "Studio launch requested." });
5020
+ }
5021
+ const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
5022
+ if (!connected) {
5023
+ try {
5024
+ this.instanceManager.close(record);
5025
+ } catch {
5026
+ }
5027
+ return this._textResult({
5028
+ error: "Studio launched, but the MCP plugin did not connect before timeout."
5029
+ });
5030
+ }
5031
+ this.instanceManager.attachInstanceId(record, connected.instanceId);
5032
+ return this._textResult({
5033
+ instance_id: connected.instanceId,
5034
+ message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
5035
+ });
5036
+ }
5037
+ async soloPlaytest(action, mode, timeout, instance_id) {
5038
+ if (action !== "start" && action !== "stop" && action !== "status") {
5039
+ throw new Error("solo_playtest requires action=start|stop|status");
5040
+ }
5041
+ if (action === "status") {
5042
+ const instanceId = this._resolveInstanceIdOnly(instance_id);
5043
+ const { roles, runtimeRoles } = this._briefRoles(instanceId, true);
5044
+ return this._textResult({
5045
+ success: true,
5046
+ action,
5047
+ running: runtimeRoles.length > 0,
5048
+ roles
5049
+ });
5050
+ }
5051
+ if (action === "start") {
5052
+ if (mode !== "play" && mode !== "run") {
5053
+ throw new Error("solo_playtest action=start requires mode=play|run");
5054
+ }
5055
+ const body2 = this._parseTextResult(await this.startPlaytest(mode, void 0, instance_id, timeout));
5056
+ if (body2.success === true && body2.runtimeReady !== false) {
5057
+ return this._textResult({
5058
+ success: true,
5059
+ action,
5060
+ message: "Playtest started.",
5061
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0
5062
+ });
5063
+ }
5064
+ return this._textResult({
5065
+ success: false,
5066
+ action,
5067
+ error: body2.error ?? "start_failed",
5068
+ message: body2.success === true ? "Playtest did not become ready before timeout." : body2.message ?? "Playtest did not start.",
5069
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0
5070
+ });
5071
+ }
5072
+ const body = this._parseTextResult(await this.stopPlaytest(instance_id, timeout));
5073
+ if (body.success === true && body.runtimeStopped !== false) {
5074
+ return this._textResult({
5075
+ success: true,
5076
+ action,
5077
+ message: "Playtest stopped."
5078
+ });
5079
+ }
5080
+ return this._textResult({
5081
+ success: false,
5082
+ action,
5083
+ error: body.error ?? "stop_failed",
5084
+ message: body.message ?? "Playtest did not stop.",
5085
+ roles: Array.isArray(body.roles) ? body.roles : void 0,
5086
+ requiresBuiltInMcp: body.requiresBuiltInMcp === true ? true : void 0,
5087
+ recoveryHint: typeof body.recoveryHint === "string" ? body.recoveryHint : void 0
5088
+ });
5089
+ }
5090
+ async startPlaytest(mode, numPlayers, instance_id, timeout = 60) {
4540
5091
  if (mode !== "play" && mode !== "run") {
4541
5092
  throw new Error('mode must be "play" or "run"');
4542
5093
  }
4543
5094
  if (numPlayers !== void 0) {
4544
- throw new Error("start_playtest is single-player only. Use multiplayer_test_start for multi-client StudioTestService sessions.");
5095
+ throw new Error('start_playtest is single-player only. Use multiplayer_playtest action="start" for multi-client StudioTestService sessions.');
4545
5096
  }
4546
5097
  const data = { mode };
4547
5098
  const startedAt = Date.now();
@@ -4580,7 +5131,7 @@ ${code}`
4580
5131
  let wait;
4581
5132
  if (response?.success === true) {
4582
5133
  const requiredRoles = mode === "play" ? ["server", "client-1"] : ["server"];
4583
- wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles, 60, true);
5134
+ wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles, timeout, true);
4584
5135
  }
4585
5136
  const body = wait ? {
4586
5137
  ...response,
@@ -4597,7 +5148,7 @@ ${code}`
4597
5148
  ]
4598
5149
  };
4599
5150
  }
4600
- async stopPlaytest(instance_id) {
5151
+ async stopPlaytest(instance_id, timeout = 15) {
4601
5152
  const { instanceId } = this._resolveSingleTarget("edit", instance_id);
4602
5153
  let response;
4603
5154
  let stopRequestError;
@@ -4613,7 +5164,7 @@ ${code}`
4613
5164
  }
4614
5165
  let wait;
4615
5166
  if (response?.success === true) {
4616
- wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, 15, true);
5167
+ wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, timeout, true);
4617
5168
  } else if (this._runtimeTargetsForEquivalentInstances(instanceId).length > 0) {
4618
5169
  wait = {
4619
5170
  ok: false,
@@ -4738,6 +5289,94 @@ ${code}`
4738
5289
  }
4739
5290
  return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
4740
5291
  }
5292
+ async multiplayerPlaytest(action, numPlayers, target, testArgs, value, timeout, instance_id) {
5293
+ if (action !== "start" && action !== "status" && action !== "add_players" && action !== "leave_client" && action !== "end") {
5294
+ throw new Error("multiplayer_playtest requires action=start|status|add_players|leave_client|end");
5295
+ }
5296
+ const briefState = async (instanceId) => {
5297
+ const state = await this._buildMultiplayerState(this._resolveInstanceIdOnly(instanceId));
5298
+ return {
5299
+ phase: state.phase,
5300
+ roles: Array.isArray(state.peers) ? state.peers.map((peer) => peer.role).filter((role) => typeof role === "string") : [],
5301
+ clientRoles: Array.isArray(state.clientRoles) ? state.clientRoles : [],
5302
+ playerCount: typeof state.playerCount === "number" ? state.playerCount : void 0,
5303
+ error: typeof state.error === "string" ? state.error : void 0
5304
+ };
5305
+ };
5306
+ if (action === "status") {
5307
+ return this._textResult({
5308
+ success: true,
5309
+ action,
5310
+ ...await briefState(instance_id)
5311
+ });
5312
+ }
5313
+ if (action === "start") {
5314
+ const body2 = this._parseTextResult(await this.multiplayerTestStart(numPlayers, testArgs, timeout, instance_id));
5315
+ const state = body2.state && typeof body2.state === "object" ? body2.state : {};
5316
+ const success = body2.success === true && body2.ready === true;
5317
+ return this._textResult(success ? {
5318
+ success: true,
5319
+ action,
5320
+ message: "Multiplayer playtest started.",
5321
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0,
5322
+ clientRoles: Array.isArray(state.clientRoles) ? state.clientRoles : void 0,
5323
+ playerCount: typeof state.playerCount === "number" ? state.playerCount : void 0
5324
+ } : {
5325
+ success: false,
5326
+ action,
5327
+ error: body2.error ?? body2.wait?.error ?? "start_failed",
5328
+ message: body2.success === true ? "Multiplayer playtest did not become ready before timeout." : body2.message ?? "Multiplayer playtest did not start.",
5329
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0
5330
+ });
5331
+ }
5332
+ if (action === "add_players") {
5333
+ const body2 = this._parseTextResult(await this.multiplayerTestAddPlayers(numPlayers, timeout, instance_id));
5334
+ const state = body2.state && typeof body2.state === "object" ? body2.state : {};
5335
+ const success = body2.success === true && body2.ready === true;
5336
+ return this._textResult(success ? {
5337
+ success: true,
5338
+ action,
5339
+ message: "Players added.",
5340
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0,
5341
+ clientRoles: Array.isArray(state.clientRoles) ? state.clientRoles : void 0,
5342
+ playerCount: typeof state.playerCount === "number" ? state.playerCount : void 0
5343
+ } : {
5344
+ success: false,
5345
+ action,
5346
+ error: body2.error ?? "add_players_failed",
5347
+ message: body2.success === true ? "Players did not finish joining before timeout." : body2.message ?? "Players were not added.",
5348
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0
5349
+ });
5350
+ }
5351
+ if (action === "leave_client") {
5352
+ const body2 = this._parseTextResult(await this.multiplayerTestLeaveClient(target ?? "client-1", timeout, instance_id));
5353
+ return this._textResult(body2.success === true && body2.left === true ? {
5354
+ success: true,
5355
+ action,
5356
+ message: "Client left.",
5357
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0
5358
+ } : {
5359
+ success: false,
5360
+ action,
5361
+ error: body2.error ?? "leave_client_failed",
5362
+ message: body2.message ?? "Client did not leave.",
5363
+ roles: Array.isArray(body2.roles) ? body2.roles : void 0
5364
+ });
5365
+ }
5366
+ const body = this._parseTextResult(await this.multiplayerTestEnd(value, timeout, instance_id));
5367
+ return this._textResult(body.success === true && body.ended === true ? {
5368
+ success: true,
5369
+ action,
5370
+ message: "Multiplayer playtest ended."
5371
+ } : {
5372
+ success: false,
5373
+ action,
5374
+ error: body.error ?? "end_failed",
5375
+ message: body.message ?? "Multiplayer playtest did not end.",
5376
+ roles: Array.isArray(body.roles) ? body.roles : void 0,
5377
+ editDone: body.editDone === false ? false : void 0
5378
+ });
5379
+ }
4741
5380
  async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id) {
4742
5381
  if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
4743
5382
  throw new Error("numPlayers must be an integer from 1 to 8");
@@ -4875,14 +5514,14 @@ ${code}`
4875
5514
  };
4876
5515
  }
4877
5516
  static findProjectRoot(startDir) {
4878
- let dir = path.resolve(startDir);
5517
+ let dir = path2.resolve(startDir);
4879
5518
  let previous = "";
4880
5519
  while (dir !== previous) {
4881
- if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json"))) {
5520
+ if (fs.existsSync(path2.join(dir, ".git")) || fs.existsSync(path2.join(dir, "package.json"))) {
4882
5521
  return dir;
4883
5522
  }
4884
5523
  previous = dir;
4885
- dir = path.dirname(dir);
5524
+ dir = path2.dirname(dir);
4886
5525
  }
4887
5526
  return null;
4888
5527
  }
@@ -4896,7 +5535,7 @@ ${code}`
4896
5535
  }
4897
5536
  }
4898
5537
  static ensureWritableDirectory(candidate, label) {
4899
- const resolved = path.resolve(candidate);
5538
+ const resolved = path2.resolve(candidate);
4900
5539
  try {
4901
5540
  fs.mkdirSync(resolved, { recursive: true });
4902
5541
  } catch (error) {
@@ -4917,11 +5556,11 @@ ${code}`
4917
5556
  if (_RobloxStudioTools._cachedLibraryPath)
4918
5557
  return _RobloxStudioTools._cachedLibraryPath;
4919
5558
  const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
4920
- const cwd = path.resolve(process.cwd());
5559
+ const cwd = path2.resolve(process.cwd());
4921
5560
  const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
4922
- const homeLibraryPath = path.join(os.homedir(), ".robloxstudio-mcp", "build-library");
4923
- const projectLibraryPath = projectRoot ? path.join(projectRoot, "build-library") : null;
4924
- const cwdLibraryPath = path.join(cwd, "build-library");
5561
+ const homeLibraryPath = path2.join(os2.homedir(), ".robloxstudio-mcp", "build-library");
5562
+ const projectLibraryPath = projectRoot ? path2.join(projectRoot, "build-library") : null;
5563
+ const cwdLibraryPath = path2.join(cwd, "build-library");
4925
5564
  let result;
4926
5565
  if (overridePath) {
4927
5566
  result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
@@ -4935,7 +5574,7 @@ ${code}`
4935
5574
  }
4936
5575
  })());
4937
5576
  if (existing) {
4938
- result = path.resolve(existing);
5577
+ result = path2.resolve(existing);
4939
5578
  } else if (projectLibraryPath) {
4940
5579
  try {
4941
5580
  result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
@@ -4962,8 +5601,8 @@ ${code}`
4962
5601
  if (response && response.success && response.buildData) {
4963
5602
  const buildData = response.buildData;
4964
5603
  const buildId = buildData.id || `${style}/exported`;
4965
- const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
4966
- const dirPath = path.dirname(filePath);
5604
+ const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
5605
+ const dirPath = path2.dirname(filePath);
4967
5606
  if (!fs.existsSync(dirPath)) {
4968
5607
  fs.mkdirSync(dirPath, { recursive: true });
4969
5608
  }
@@ -5064,8 +5703,8 @@ ${code}`
5064
5703
  const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
5065
5704
  const computedBounds = bounds || this.computeBounds(normalizedParts);
5066
5705
  const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
5067
- const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
5068
- const dirPath = path.dirname(filePath);
5706
+ const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
5707
+ const dirPath = path2.dirname(filePath);
5069
5708
  if (!fs.existsSync(dirPath)) {
5070
5709
  fs.mkdirSync(dirPath, { recursive: true });
5071
5710
  }
@@ -5123,8 +5762,8 @@ ${code}`
5123
5762
  };
5124
5763
  if (seed !== void 0)
5125
5764
  buildData.generatorSeed = seed;
5126
- const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
5127
- const dirPath = path.dirname(filePath);
5765
+ const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
5766
+ const dirPath = path2.dirname(filePath);
5128
5767
  if (!fs.existsSync(dirPath)) {
5129
5768
  fs.mkdirSync(dirPath, { recursive: true });
5130
5769
  }
@@ -5152,13 +5791,13 @@ ${code}`
5152
5791
  }
5153
5792
  let resolved;
5154
5793
  if (typeof buildData === "string") {
5155
- const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
5794
+ const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
5156
5795
  if (!fs.existsSync(filePath)) {
5157
5796
  throw new Error(`Build not found in library: ${buildData}`);
5158
5797
  }
5159
5798
  resolved = JSON.parse(fs.readFileSync(filePath, "utf-8"));
5160
5799
  } else if (buildData.id && !buildData.parts) {
5161
- const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
5800
+ const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
5162
5801
  if (!fs.existsSync(filePath)) {
5163
5802
  throw new Error(`Build not found in library: ${buildData.id}`);
5164
5803
  }
@@ -5185,13 +5824,13 @@ ${code}`
5185
5824
  const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
5186
5825
  const builds = [];
5187
5826
  for (const s of styles) {
5188
- const dirPath = path.join(libraryPath, s);
5827
+ const dirPath = path2.join(libraryPath, s);
5189
5828
  if (!fs.existsSync(dirPath))
5190
5829
  continue;
5191
5830
  const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
5192
5831
  for (const file of files) {
5193
5832
  try {
5194
- const content = fs.readFileSync(path.join(dirPath, file), "utf-8");
5833
+ const content = fs.readFileSync(path2.join(dirPath, file), "utf-8");
5195
5834
  const data = JSON.parse(content);
5196
5835
  builds.push({
5197
5836
  id: data.id || `${s}/${file.replace(".json", "")}`,
@@ -5230,7 +5869,7 @@ ${code}`
5230
5869
  if (!id) {
5231
5870
  throw new Error("Build ID is required for get_build");
5232
5871
  }
5233
- const filePath = path.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
5872
+ const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
5234
5873
  if (!fs.existsSync(filePath)) {
5235
5874
  throw new Error(`Build not found in library: ${id}`);
5236
5875
  }
@@ -5317,7 +5956,7 @@ ${code}`
5317
5956
  if (!buildId) {
5318
5957
  throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
5319
5958
  }
5320
- const filePath = path.join(libraryPath, `${buildId}.json`);
5959
+ const filePath = path2.join(libraryPath, `${buildId}.json`);
5321
5960
  if (!fs.existsSync(filePath)) {
5322
5961
  throw new Error(`Build not found in library: ${buildId}`);
5323
5962
  }
@@ -5500,7 +6139,7 @@ ${code}`
5500
6139
  throw new Error(`File not found: ${filePath}`);
5501
6140
  }
5502
6141
  const fileContent = fs.readFileSync(filePath);
5503
- const fileName = path.basename(filePath);
6142
+ const fileName = path2.basename(filePath);
5504
6143
  if (assetType === "Decal" && this.cookieClient.hasCookie()) {
5505
6144
  const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
5506
6145
  return {
@@ -5725,9 +6364,9 @@ ${code}`
5725
6364
  return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
5726
6365
  }
5727
6366
  const bytes = Buffer.from(response.base64, "base64");
5728
- const resolved = path.resolve(outputPath);
6367
+ const resolved = path2.resolve(outputPath);
5729
6368
  try {
5730
- fs.mkdirSync(path.dirname(resolved), { recursive: true });
6369
+ fs.mkdirSync(path2.dirname(resolved), { recursive: true });
5731
6370
  fs.writeFileSync(resolved, bytes);
5732
6371
  } catch (err) {
5733
6372
  return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
@@ -5761,7 +6400,7 @@ ${code}`
5761
6400
  let bytes;
5762
6401
  let sourceLabel;
5763
6402
  if (source.path !== void 0) {
5764
- const resolved = path.resolve(source.path);
6403
+ const resolved = path2.resolve(source.path);
5765
6404
  try {
5766
6405
  bytes = fs.readFileSync(resolved);
5767
6406
  } catch (err) {
@@ -5830,7 +6469,7 @@ ${code}`
5830
6469
  if (response.error) {
5831
6470
  let text = response.error;
5832
6471
  if (targetRole.startsWith("client-") && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
5833
- text = `Screenshot capture reached the multiplayer client, but Roblox returned a temporary screenshot texture that the edit peer cannot read in StudioTestService multiplayer sessions. Regular start_playtest capture works because the temporary rbxtemp:// handle is readable from the edit process; multiplayer client handles appear to be scoped to the client process. Raw error: ${response.error}`;
6472
+ text = `Screenshot capture reached the multiplayer client, but Roblox returned a temporary screenshot texture that the edit peer cannot read in StudioTestService multiplayer sessions. Regular solo_playtest capture works because the temporary rbxtemp:// handle is readable from the edit process; multiplayer client handles appear to be scoped to the client process. Raw error: ${response.error}`;
5834
6473
  }
5835
6474
  return { success: false, error: text };
5836
6475
  }
@@ -6003,7 +6642,7 @@ var init_server = __esm({
6003
6642
  config;
6004
6643
  constructor(config) {
6005
6644
  this.config = config;
6006
- this.allowedToolNames = new Set(config.tools.map((t) => t.name));
6645
+ this.allowedToolNames = new Set((config.callableTools ?? config.tools).map((t) => t.name));
6007
6646
  this.server = new Server2({
6008
6647
  name: config.name,
6009
6648
  version: config.version
@@ -6176,7 +6815,7 @@ var init_server = __esm({
6176
6815
  });
6177
6816
 
6178
6817
  // ../core/dist/tools/definitions.js
6179
- var TOOL_DEFINITIONS, getAllTools;
6818
+ var TOOL_DEFINITIONS, DEPRECATED_TOOL_DEFINITIONS, getAllTools, getAllCallableTools;
6180
6819
  var init_definitions = __esm({
6181
6820
  "../core/dist/tools/definitions.js"() {
6182
6821
  "use strict";
@@ -7142,43 +7781,92 @@ var init_definitions = __esm({
7142
7781
  required: ["pattern"]
7143
7782
  }
7144
7783
  },
7145
- // === Playtest ===
7784
+ // === Studio Instance Management ===
7146
7785
  {
7147
- name: "start_playtest",
7786
+ name: "manage_instance",
7148
7787
  category: "write",
7149
- description: "Start a simple single-player Studio playtest in play or run mode, waiting until a runtime peer registers with MCP. Read print/warn/error output with get_runtime_logs, then end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
7788
+ description: 'Launch, close, inspect, and find revisions for Studio instances. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision" and place_version to open an older revision. action="close" can close an MCP-managed instance or an explicitly connected edit instance by instance_id. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
7150
7789
  inputSchema: {
7151
7790
  type: "object",
7152
7791
  properties: {
7153
- mode: {
7792
+ action: {
7154
7793
  type: "string",
7155
- enum: ["play", "run"],
7156
- description: "Play mode"
7794
+ enum: ["launch", "close", "status", "list_place_versions"],
7795
+ description: "Instance management action."
7157
7796
  },
7158
- numPlayers: {
7797
+ source: {
7798
+ type: "string",
7799
+ enum: ["baseplate", "local_file", "published_place", "place_revision"],
7800
+ description: 'Required for action="launch". published_place opens the latest place; place_revision opens a specific older version as an anonymous local copy.'
7801
+ },
7802
+ local_place_file: {
7803
+ type: "string",
7804
+ description: 'Required for source="local_file". Path to a .rbxl/.rbxlx place file.'
7805
+ },
7806
+ place_id: {
7807
+ type: "number",
7808
+ description: 'Required for source="published_place", source="place_revision", and action="list_place_versions".'
7809
+ },
7810
+ universe_id: {
7811
+ type: "number",
7812
+ description: "Optional for published_place/place_revision launches; derived from place_id when omitted."
7813
+ },
7814
+ place_version: {
7815
+ type: "number",
7816
+ description: 'Required for source="place_revision". Use action="list_place_versions" to discover available version numbers.'
7817
+ },
7818
+ wait_for_connection: {
7819
+ type: "boolean",
7820
+ description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true).'
7821
+ },
7822
+ timeout_ms: {
7159
7823
  type: "number",
7160
- description: "Deprecated and rejected. Use multiplayer_test_start for multi-client testing."
7824
+ description: 'For action="launch": max milliseconds to wait for plugin connection (default 120000).'
7825
+ },
7826
+ max_page_size: {
7827
+ type: "number",
7828
+ description: 'For action="list_place_versions": number of versions to return, clamped to 1-50 (default 10).'
7829
+ },
7830
+ page_token: {
7831
+ type: "string",
7832
+ description: 'For action="list_place_versions": pagination token returned by a prior call.'
7161
7833
  },
7162
7834
  instance_id: {
7163
7835
  type: "string",
7164
- description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7836
+ description: 'For action="close" or action="status": Studio instance to inspect or close. close accepts MCP-managed instances and explicitly connected edit instances.'
7165
7837
  }
7166
7838
  },
7167
- required: ["mode"]
7839
+ required: ["action"]
7168
7840
  }
7169
7841
  },
7842
+ // === Playtest ===
7170
7843
  {
7171
- name: "stop_playtest",
7844
+ name: "solo_playtest",
7172
7845
  category: "write",
7173
- description: "Stop playtest and wait for runtime peers to disconnect.",
7846
+ description: 'Start, stop, or inspect a single-player Studio playtest. Use action="start" with mode="play" or "run", action="stop" to end the playtest, and action="status" to inspect active runtime roles. Returns brief lifecycle status only; read script output with get_runtime_logs. Ordinary start/eval/stop workflows do not need reset_simulation_state; use simulation reset only for network or device-simulator tests. For multi-client testing use multiplayer_playtest.',
7174
7847
  inputSchema: {
7175
7848
  type: "object",
7176
7849
  properties: {
7850
+ action: {
7851
+ type: "string",
7852
+ enum: ["start", "stop", "status"],
7853
+ description: "Lifecycle action to run."
7854
+ },
7855
+ mode: {
7856
+ type: "string",
7857
+ enum: ["play", "run"],
7858
+ description: 'Required for action="start".'
7859
+ },
7860
+ timeout: {
7861
+ type: "number",
7862
+ description: "Max seconds to wait for start readiness or stop teardown. Defaults: start 60, stop 15."
7863
+ },
7177
7864
  instance_id: {
7178
7865
  type: "string",
7179
7866
  description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7180
7867
  }
7181
- }
7868
+ },
7869
+ required: ["action"]
7182
7870
  }
7183
7871
  },
7184
7872
  {
@@ -7247,7 +7935,7 @@ var init_definitions = __esm({
7247
7935
  {
7248
7936
  name: "get_simulation_state",
7249
7937
  category: "read",
7250
- description: 'Inspect current NetworkSettings and/or StudioDeviceSimulatorService state for edit and connected playtest clients only. Defaults to include="both" and target="edit-and-clients"; server peers are skipped. Use before diagnosing network or device-sensitive tests, especially because normal Play can write client simulator changes back to edit and StudioTestService clients can inherit stale device simulator state.',
7938
+ description: 'Inspect current NetworkSettings and/or StudioDeviceSimulatorService state for edit and connected clients only. Defaults to include="both" and target="edit-and-clients"; server peers are skipped. Use when a task explicitly involves simulated network/device behavior or when you suspect stale simulator state. This is not part of ordinary playtest lifecycle.',
7251
7939
  inputSchema: {
7252
7940
  type: "object",
7253
7941
  properties: {
@@ -7270,7 +7958,7 @@ var init_definitions = __esm({
7270
7958
  {
7271
7959
  name: "reset_simulation_state",
7272
7960
  category: "write",
7273
- description: 'Reset reachable simulation state to a clean baseline for deterministic tests. Defaults to target="edit-and-clients" and resets both network and device simulator state. Network reset sets all six simulated NetworkSettings fields to 0; device reset calls StopSimulationAsync(). Call before tests, after starting Play or multiplayer, before stopping, and again on edit after stopping.',
7961
+ description: 'Reset reachable NetworkSettings and/or StudioDeviceSimulatorService state for deterministic network/device tests. Defaults to target="edit-and-clients" and resets both network and device simulator state. Network reset sets all six simulated NetworkSettings fields to 0; device reset calls StopSimulationAsync(). Do not call as routine Studio lifecycle hygiene. Use it after intentionally changing simulation settings, when get_simulation_state shows dirty state, or when a task explicitly requires a clean network/device baseline.',
7274
7962
  inputSchema: {
7275
7963
  type: "object",
7276
7964
  properties: {
@@ -7456,109 +8144,41 @@ var init_definitions = __esm({
7456
8144
  }
7457
8145
  },
7458
8146
  {
7459
- name: "multiplayer_test_start",
8147
+ name: "multiplayer_playtest",
7460
8148
  category: "write",
7461
- description: "Start a StudioTestService multiplayer test and wait for the server plus requested client peers to connect. Use this for multi-client runtime testing.",
7462
- inputSchema: {
7463
- type: "object",
7464
- properties: {
7465
- numPlayers: {
7466
- type: "number",
7467
- description: "Number of client players to start (1-8)."
7468
- },
7469
- testArgs: {
7470
- description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
7471
- },
7472
- timeout: {
7473
- type: "number",
7474
- description: "Max seconds to wait for server + clients to register (default 30)."
7475
- },
7476
- instance_id: {
7477
- type: "string",
7478
- description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7479
- }
7480
- },
7481
- required: ["numPlayers"]
7482
- }
7483
- },
7484
- {
7485
- name: "multiplayer_test_state",
7486
- category: "read",
7487
- description: "Get the active multiplayer StudioTestService state for a place: phase, peers, players, original testArgs, result/error, and connected client roles.",
8149
+ description: 'Start, inspect, add players to, remove a client from, or end a StudioTestService multiplayer playtest. Use action="start" with numPlayers, action="status", action="add_players" with numPlayers, action="leave_client" with target="client-N", or action="end". Returns brief lifecycle status only; read script output with get_runtime_logs.',
7488
8150
  inputSchema: {
7489
8151
  type: "object",
7490
8152
  properties: {
7491
- instance_id: {
8153
+ action: {
7492
8154
  type: "string",
7493
- description: "Which connected Studio place to inspect. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7494
- }
7495
- }
7496
- }
7497
- },
7498
- {
7499
- name: "multiplayer_test_add_players",
7500
- category: "write",
7501
- description: "Add client players to a running StudioTestService multiplayer test and wait for the new clients to connect.",
7502
- inputSchema: {
7503
- type: "object",
7504
- properties: {
8155
+ enum: ["start", "status", "add_players", "leave_client", "end"],
8156
+ description: "Lifecycle action to run."
8157
+ },
7505
8158
  numPlayers: {
7506
8159
  type: "number",
7507
- description: "Number of additional client players to add (1-8)."
8160
+ description: 'Required for action="start" and action="add_players". Number of client players (1-8).'
7508
8161
  },
7509
- timeout: {
7510
- type: "number",
7511
- description: "Max seconds to wait for new clients to register (default 30)."
7512
- },
7513
- instance_id: {
7514
- type: "string",
7515
- description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7516
- }
7517
- },
7518
- required: ["numPlayers"]
7519
- }
7520
- },
7521
- {
7522
- name: "multiplayer_test_leave_client",
7523
- category: "write",
7524
- description: "Disconnect a specific client from a running StudioTestService multiplayer test, then wait for that client peer to leave.",
7525
- inputSchema: {
7526
- type: "object",
7527
- properties: {
7528
8162
  target: {
7529
8163
  type: "string",
7530
- description: 'Client target to leave: "client-1" (default), "client-2", etc.'
8164
+ description: 'Client target for action="leave_client", such as "client-1". Defaults to "client-1".'
7531
8165
  },
7532
- timeout: {
7533
- type: "number",
7534
- description: "Max seconds to wait for the client peer to disconnect (default 30)."
8166
+ testArgs: {
8167
+ description: 'For action="start": JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients.'
7535
8168
  },
7536
- instance_id: {
7537
- type: "string",
7538
- description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7539
- }
7540
- }
7541
- }
7542
- },
7543
- {
7544
- name: "multiplayer_test_end",
7545
- category: "write",
7546
- description: "End a running StudioTestService multiplayer test with an optional return value, then wait for all runtime peers to disconnect.",
7547
- inputSchema: {
7548
- type: "object",
7549
- properties: {
7550
8169
  value: {
7551
- description: "JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call."
8170
+ description: 'For action="end": JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call.'
7552
8171
  },
7553
8172
  timeout: {
7554
8173
  type: "number",
7555
- description: "Max seconds to wait for runtime peers to disconnect (default 30)."
8174
+ description: "Max seconds to wait for action completion. Defaults to 30."
7556
8175
  },
7557
8176
  instance_id: {
7558
8177
  type: "string",
7559
8178
  description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7560
8179
  }
7561
- }
8180
+ },
8181
+ required: ["action"]
7562
8182
  }
7563
8183
  },
7564
8184
  {
@@ -8586,20 +9206,168 @@ part(0,2,0,2,1,1,"b")`,
8586
9206
  }
8587
9207
  }
8588
9208
  ];
9209
+ DEPRECATED_TOOL_DEFINITIONS = [
9210
+ // === Deprecated Playtest API ===
9211
+ {
9212
+ name: "start_playtest",
9213
+ category: "write",
9214
+ description: 'Deprecated. Use solo_playtest with action="start" instead. Starts a simple single-player Studio playtest in play or run mode.',
9215
+ inputSchema: {
9216
+ type: "object",
9217
+ properties: {
9218
+ mode: {
9219
+ type: "string",
9220
+ enum: ["play", "run"],
9221
+ description: "Play mode"
9222
+ },
9223
+ numPlayers: {
9224
+ type: "number",
9225
+ description: 'Deprecated and rejected. Use multiplayer_playtest action="start" for multi-client testing.'
9226
+ },
9227
+ instance_id: {
9228
+ type: "string",
9229
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
9230
+ }
9231
+ },
9232
+ required: ["mode"]
9233
+ }
9234
+ },
9235
+ {
9236
+ name: "stop_playtest",
9237
+ category: "write",
9238
+ description: 'Deprecated. Use solo_playtest with action="stop" instead. Stops a single-player Studio playtest and waits for runtime peers to disconnect.',
9239
+ inputSchema: {
9240
+ type: "object",
9241
+ properties: {
9242
+ instance_id: {
9243
+ type: "string",
9244
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
9245
+ }
9246
+ }
9247
+ }
9248
+ },
9249
+ {
9250
+ name: "multiplayer_test_start",
9251
+ category: "write",
9252
+ description: 'Deprecated. Use multiplayer_playtest with action="start" instead. Starts a StudioTestService multiplayer test.',
9253
+ inputSchema: {
9254
+ type: "object",
9255
+ properties: {
9256
+ numPlayers: {
9257
+ type: "number",
9258
+ description: "Number of client players to start (1-8)."
9259
+ },
9260
+ testArgs: {
9261
+ description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
9262
+ },
9263
+ timeout: {
9264
+ type: "number",
9265
+ description: "Max seconds to wait for server + clients to register (default 30)."
9266
+ },
9267
+ instance_id: {
9268
+ type: "string",
9269
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
9270
+ }
9271
+ },
9272
+ required: ["numPlayers"]
9273
+ }
9274
+ },
9275
+ {
9276
+ name: "multiplayer_test_state",
9277
+ category: "read",
9278
+ description: 'Deprecated. Use multiplayer_playtest with action="status" instead. Gets the active multiplayer StudioTestService state.',
9279
+ inputSchema: {
9280
+ type: "object",
9281
+ properties: {
9282
+ instance_id: {
9283
+ type: "string",
9284
+ description: "Which connected Studio place to inspect. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
9285
+ }
9286
+ }
9287
+ }
9288
+ },
9289
+ {
9290
+ name: "multiplayer_test_add_players",
9291
+ category: "write",
9292
+ description: 'Deprecated. Use multiplayer_playtest with action="add_players" instead. Adds client players to a running StudioTestService multiplayer test.',
9293
+ inputSchema: {
9294
+ type: "object",
9295
+ properties: {
9296
+ numPlayers: {
9297
+ type: "number",
9298
+ description: "Number of additional client players to add (1-8)."
9299
+ },
9300
+ timeout: {
9301
+ type: "number",
9302
+ description: "Max seconds to wait for new clients to register (default 30)."
9303
+ },
9304
+ instance_id: {
9305
+ type: "string",
9306
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
9307
+ }
9308
+ },
9309
+ required: ["numPlayers"]
9310
+ }
9311
+ },
9312
+ {
9313
+ name: "multiplayer_test_leave_client",
9314
+ category: "write",
9315
+ description: 'Deprecated. Use multiplayer_playtest with action="leave_client" instead. Disconnects a specific client from a running StudioTestService multiplayer test.',
9316
+ inputSchema: {
9317
+ type: "object",
9318
+ properties: {
9319
+ target: {
9320
+ type: "string",
9321
+ description: 'Client target to leave: "client-1" (default), "client-2", etc.'
9322
+ },
9323
+ timeout: {
9324
+ type: "number",
9325
+ description: "Max seconds to wait for the client peer to disconnect (default 30)."
9326
+ },
9327
+ instance_id: {
9328
+ type: "string",
9329
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
9330
+ }
9331
+ }
9332
+ }
9333
+ },
9334
+ {
9335
+ name: "multiplayer_test_end",
9336
+ category: "write",
9337
+ description: 'Deprecated. Use multiplayer_playtest with action="end" instead. Ends a running StudioTestService multiplayer test.',
9338
+ inputSchema: {
9339
+ type: "object",
9340
+ properties: {
9341
+ value: {
9342
+ description: "JSON-compatible value returned to the edit-side ExecuteMultiplayerTestAsync call."
9343
+ },
9344
+ timeout: {
9345
+ type: "number",
9346
+ description: "Max seconds to wait for runtime peers to disconnect (default 30)."
9347
+ },
9348
+ instance_id: {
9349
+ type: "string",
9350
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
9351
+ }
9352
+ }
9353
+ }
9354
+ }
9355
+ ];
8589
9356
  getAllTools = () => [...TOOL_DEFINITIONS];
9357
+ getAllCallableTools = () => [...TOOL_DEFINITIONS, ...DEPRECATED_TOOL_DEFINITIONS];
8590
9358
  }
8591
9359
  });
8592
9360
 
8593
9361
  // ../core/dist/install-plugin-helpers.js
8594
- import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync } from "fs";
9362
+ import { existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync } from "fs";
8595
9363
  import { execSync } from "child_process";
8596
- import { join as join2 } from "path";
8597
- import { homedir as homedir2 } from "os";
9364
+ import { join as join3 } from "path";
9365
+ import { homedir as homedir3 } from "os";
8598
9366
  function isWSL() {
8599
9367
  if (process.platform !== "linux")
8600
9368
  return false;
8601
9369
  try {
8602
- const v = readFileSync2("/proc/version", "utf8");
9370
+ const v = readFileSync3("/proc/version", "utf8");
8603
9371
  return /microsoft|wsl/i.test(v);
8604
9372
  } catch {
8605
9373
  return false;
@@ -8617,7 +9385,7 @@ function getWindowsUserPluginsDir() {
8617
9385
  }).toString().trim();
8618
9386
  if (!linuxPath)
8619
9387
  return null;
8620
- return join2(linuxPath, "Roblox", "Plugins");
9388
+ return join3(linuxPath, "Roblox", "Plugins");
8621
9389
  } catch {
8622
9390
  return null;
8623
9391
  }
@@ -8626,7 +9394,7 @@ function getPluginsFolder() {
8626
9394
  if (process.env.MCP_PLUGINS_DIR)
8627
9395
  return process.env.MCP_PLUGINS_DIR;
8628
9396
  if (process.platform === "win32") {
8629
- return join2(process.env.LOCALAPPDATA || join2(homedir2(), "AppData", "Local"), "Roblox", "Plugins");
9397
+ return join3(process.env.LOCALAPPDATA || join3(homedir3(), "AppData", "Local"), "Roblox", "Plugins");
8630
9398
  }
8631
9399
  if (isWSL()) {
8632
9400
  const win = getWindowsUserPluginsDir();
@@ -8634,11 +9402,11 @@ function getPluginsFolder() {
8634
9402
  return win;
8635
9403
  console.warn("[install-plugin] WSL detected but could not resolve Windows %LOCALAPPDATA%. Falling back to ~/Documents/Roblox/Plugins/ - you will likely need to copy the rbxmx to /mnt/c/Users/<you>/AppData/Local/Roblox/Plugins/ manually. Set MCP_PLUGINS_DIR to skip detection.");
8636
9404
  }
8637
- return join2(homedir2(), "Documents", "Roblox", "Plugins");
9405
+ return join3(homedir3(), "Documents", "Roblox", "Plugins");
8638
9406
  }
8639
9407
  function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
8640
- const otherDest = join2(pluginsFolder, otherAssetName);
8641
- if (!existsSync2(otherDest))
9408
+ const otherDest = join3(pluginsFolder, otherAssetName);
9409
+ if (!existsSync3(otherDest))
8642
9410
  return;
8643
9411
  if (replace) {
8644
9412
  try {
@@ -8683,8 +9451,8 @@ __export(install_plugin_exports, {
8683
9451
  installBundledPlugin: () => installBundledPlugin,
8684
9452
  installPlugin: () => installPlugin
8685
9453
  });
8686
- import { copyFileSync, createWriteStream, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync2 } from "fs";
8687
- import { dirname as dirname2, join as join3 } from "path";
9454
+ import { copyFileSync, createWriteStream, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
9455
+ import { dirname as dirname2, join as join4 } from "path";
8688
9456
  import { fileURLToPath } from "url";
8689
9457
  import { get } from "https";
8690
9458
  function httpsGet(url) {
@@ -8754,7 +9522,7 @@ function prepareInstall({
8754
9522
  warn
8755
9523
  }) {
8756
9524
  const pluginsFolder = getPluginsFolder();
8757
- if (!existsSync3(pluginsFolder)) {
9525
+ if (!existsSync4(pluginsFolder)) {
8758
9526
  mkdirSync2(pluginsFolder, { recursive: true });
8759
9527
  }
8760
9528
  handleVariantConflict({
@@ -8769,21 +9537,21 @@ function prepareInstall({
8769
9537
  function bundledAssetPath() {
8770
9538
  const currentDir = dirname2(fileURLToPath(import.meta.url));
8771
9539
  const candidates = [
8772
- join3(currentDir, "..", "studio-plugin", ASSET_NAME),
8773
- join3(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
9540
+ join4(currentDir, "..", "studio-plugin", ASSET_NAME),
9541
+ join4(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
8774
9542
  ];
8775
- return candidates.find((candidate) => existsSync3(candidate)) ?? null;
9543
+ return candidates.find((candidate) => existsSync4(candidate)) ?? null;
8776
9544
  }
8777
9545
  function packageVersion() {
8778
9546
  const currentDir = dirname2(fileURLToPath(import.meta.url));
8779
- const pkg = JSON.parse(readFileSync3(join3(currentDir, "..", "package.json"), "utf8"));
9547
+ const pkg = JSON.parse(readFileSync4(join4(currentDir, "..", "package.json"), "utf8"));
8780
9548
  if (!pkg.version) {
8781
9549
  throw new Error("Package version not found");
8782
9550
  }
8783
9551
  return pkg.version;
8784
9552
  }
8785
9553
  function bundledPluginVersion(source) {
8786
- const match = readFileSync3(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
9554
+ const match = readFileSync4(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
8787
9555
  return match ? match[1] : null;
8788
9556
  }
8789
9557
  function assertBundledPluginVersion(source) {
@@ -8796,9 +9564,9 @@ function assertBundledPluginVersion(source) {
8796
9564
  }
8797
9565
  }
8798
9566
  function filesMatch(a, b) {
8799
- if (!existsSync3(b)) return false;
8800
- const aBytes = readFileSync3(a);
8801
- const bBytes = readFileSync3(b);
9567
+ if (!existsSync4(b)) return false;
9568
+ const aBytes = readFileSync4(a);
9569
+ const bBytes = readFileSync4(b);
8802
9570
  return aBytes.length === bBytes.length && aBytes.equals(bBytes);
8803
9571
  }
8804
9572
  async function installBundledPlugin(options = {}) {
@@ -8811,7 +9579,7 @@ async function installBundledPlugin(options = {}) {
8811
9579
  }
8812
9580
  assertBundledPluginVersion(source);
8813
9581
  const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
8814
- const dest = join3(pluginsFolder, ASSET_NAME);
9582
+ const dest = join4(pluginsFolder, ASSET_NAME);
8815
9583
  if (filesMatch(source, dest)) return;
8816
9584
  copyFileSync(source, dest);
8817
9585
  log(`Installed ${ASSET_NAME} to ${dest}`);
@@ -8825,7 +9593,7 @@ async function installPlugin(options = {}) {
8825
9593
  const bundled = bundledAssetPath();
8826
9594
  if (bundled) {
8827
9595
  assertBundledPluginVersion(bundled);
8828
- const dest2 = join3(pluginsFolder, ASSET_NAME);
9596
+ const dest2 = join4(pluginsFolder, ASSET_NAME);
8829
9597
  if (filesMatch(bundled, dest2)) {
8830
9598
  log(`${ASSET_NAME} already installed.`);
8831
9599
  return;
@@ -8840,7 +9608,7 @@ async function installPlugin(options = {}) {
8840
9608
  if (!asset) {
8841
9609
  throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
8842
9610
  }
8843
- const dest = join3(pluginsFolder, ASSET_NAME);
9611
+ const dest = join4(pluginsFolder, ASSET_NAME);
8844
9612
  log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
8845
9613
  await download(asset.browser_download_url, dest);
8846
9614
  log(`Installed to ${dest}`);
@@ -8894,7 +9662,8 @@ if (process.argv.includes("--install-plugin")) {
8894
9662
  const server = new RobloxStudioMCPServer({
8895
9663
  name: "robloxstudio-mcp",
8896
9664
  version: VERSION,
8897
- tools: getAllTools()
9665
+ tools: getAllTools(),
9666
+ callableTools: getAllCallableTools()
8898
9667
  });
8899
9668
  server.run().catch((error) => {
8900
9669
  console.error("Server failed to start:", error);