@chrrxs/robloxstudio-mcp 2.17.0 → 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 +968 -253
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +361 -253
- package/studio-plugin/MCPPlugin.rbxmx +361 -253
- package/studio-plugin/src/modules/Communication.ts +7 -5
- package/studio-plugin/src/modules/ServerUrlSettings.ts +62 -9
- package/studio-plugin/src/modules/UI.ts +11 -4
- package/studio-plugin/src/modules/Utils.ts +147 -13
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +3 -0
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +1 -177
- package/studio-plugin/src/server/index.server.ts +18 -5
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),
|
|
@@ -932,7 +943,6 @@ var init_http_server = __esm({
|
|
|
932
943
|
capture_screenshot: (tools, body) => tools.captureScreenshot(body.instance_id, body.format, body.quality),
|
|
933
944
|
simulate_mouse_input: (tools, body) => tools.simulateMouseInput(body.action, body.x, body.y, body.button, body.scrollDirection, body.target, body.instance_id),
|
|
934
945
|
simulate_keyboard_input: (tools, body) => tools.simulateKeyboardInput(body.keyCode, body.action, body.duration, body.text, body.target, body.instance_id),
|
|
935
|
-
character_navigation: (tools, body) => tools.characterNavigation(body.position, body.instancePath, body.waitForCompletion, body.timeout, body.target, body.instance_id),
|
|
936
946
|
get_memory_breakdown: (tools, body) => tools.getMemoryBreakdown(body.target, body.tags, body.instance_id),
|
|
937
947
|
get_scene_analysis: (tools, body) => tools.getSceneAnalysis(body.mode, body.target, body.topN, body.raw, body.instance_id),
|
|
938
948
|
export_rbxm: (tools, body) => tools.exportRbxm(body.instance_paths, body.output_path, body.target, body.instance_id),
|
|
@@ -1470,6 +1480,14 @@ var init_opencloud_client = __esm({
|
|
|
1470
1480
|
async getAssetDetails(assetId) {
|
|
1471
1481
|
return this.request(`/toolbox-service/v2/assets/${assetId}`);
|
|
1472
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
|
+
}
|
|
1473
1491
|
async getAssetThumbnail(assetId, size = "420x420") {
|
|
1474
1492
|
const url = `https://thumbnails.roblox.com/v1/assets?assetIds=${assetId}&size=${size}&format=Png`;
|
|
1475
1493
|
try {
|
|
@@ -1698,6 +1716,254 @@ var init_roblox_cookie_client = __esm({
|
|
|
1698
1716
|
}
|
|
1699
1717
|
});
|
|
1700
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
|
+
|
|
1701
1967
|
// ../core/dist/jpeg-encoder.js
|
|
1702
1968
|
function rgbaToJpeg(rgba, width, height, quality = 80) {
|
|
1703
1969
|
if (width <= 0 || height <= 0)
|
|
@@ -2716,8 +2982,8 @@ var init_png_encoder = __esm({
|
|
|
2716
2982
|
|
|
2717
2983
|
// ../core/dist/tools/index.js
|
|
2718
2984
|
import * as fs from "fs";
|
|
2719
|
-
import * as
|
|
2720
|
-
import * as
|
|
2985
|
+
import * as os2 from "os";
|
|
2986
|
+
import * as path2 from "path";
|
|
2721
2987
|
function encodeImageFromRgbaResponse(response, format, quality) {
|
|
2722
2988
|
if (!response.data || response.width === void 0 || response.height === void 0) {
|
|
2723
2989
|
throw new Error("Render response missing data, width, or height");
|
|
@@ -3073,6 +3339,7 @@ var init_tools = __esm({
|
|
|
3073
3339
|
init_build_executor();
|
|
3074
3340
|
init_opencloud_client();
|
|
3075
3341
|
init_roblox_cookie_client();
|
|
3342
|
+
init_studio_instance_manager();
|
|
3076
3343
|
init_jpeg_encoder();
|
|
3077
3344
|
init_png_encoder();
|
|
3078
3345
|
MAX_INLINE_IMAGE_BYTES = 6e6;
|
|
@@ -3130,11 +3397,34 @@ var init_tools = __esm({
|
|
|
3130
3397
|
bridge;
|
|
3131
3398
|
openCloudClient;
|
|
3132
3399
|
cookieClient;
|
|
3400
|
+
instanceManager;
|
|
3133
3401
|
constructor(bridge) {
|
|
3134
3402
|
this.client = new StudioHttpClient(bridge);
|
|
3135
3403
|
this.bridge = bridge;
|
|
3136
3404
|
this.openCloudClient = new OpenCloudClient();
|
|
3137
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
|
+
};
|
|
3138
3428
|
}
|
|
3139
3429
|
// Resolve (instance_id, target-role) → concrete (instanceId, role) and
|
|
3140
3430
|
// dispatch a single request. Throws RoutingFailure if the resolution is
|
|
@@ -3459,8 +3749,8 @@ var init_tools = __esm({
|
|
|
3459
3749
|
timedOut: true
|
|
3460
3750
|
};
|
|
3461
3751
|
}
|
|
3462
|
-
async getFileTree(
|
|
3463
|
-
const response = await this._callSingle("/api/file-tree", { path:
|
|
3752
|
+
async getFileTree(path3 = "", instance_id) {
|
|
3753
|
+
const response = await this._callSingle("/api/file-tree", { path: path3 }, void 0, instance_id);
|
|
3464
3754
|
return {
|
|
3465
3755
|
content: [
|
|
3466
3756
|
{
|
|
@@ -3577,9 +3867,9 @@ var init_tools = __esm({
|
|
|
3577
3867
|
]
|
|
3578
3868
|
};
|
|
3579
3869
|
}
|
|
3580
|
-
async getProjectStructure(
|
|
3870
|
+
async getProjectStructure(path3, maxDepth, scriptsOnly, instance_id) {
|
|
3581
3871
|
const response = await this._callSingle("/api/project-structure", {
|
|
3582
|
-
path:
|
|
3872
|
+
path: path3,
|
|
3583
3873
|
maxDepth,
|
|
3584
3874
|
scriptsOnly
|
|
3585
3875
|
}, void 0, instance_id);
|
|
@@ -4500,8 +4790,8 @@ ${code}`
|
|
|
4500
4790
|
const rawJson = mutable.raw_json;
|
|
4501
4791
|
if (typeof rawJson === "string") {
|
|
4502
4792
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
4503
|
-
const resolvedOutputPath =
|
|
4504
|
-
fs.mkdirSync(
|
|
4793
|
+
const resolvedOutputPath = path2.resolve(outputPath);
|
|
4794
|
+
fs.mkdirSync(path2.dirname(resolvedOutputPath), { recursive: true });
|
|
4505
4795
|
fs.writeFileSync(resolvedOutputPath, rawJson, "utf8");
|
|
4506
4796
|
mutable.output_path = resolvedOutputPath;
|
|
4507
4797
|
}
|
|
@@ -4537,12 +4827,272 @@ ${code}`
|
|
|
4537
4827
|
const body = response !== null && typeof response === "object" && !Array.isArray(response) ? { ...response, target: resolved.targetRole } : response;
|
|
4538
4828
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
4539
4829
|
}
|
|
4540
|
-
|
|
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) {
|
|
4541
5091
|
if (mode !== "play" && mode !== "run") {
|
|
4542
5092
|
throw new Error('mode must be "play" or "run"');
|
|
4543
5093
|
}
|
|
4544
5094
|
if (numPlayers !== void 0) {
|
|
4545
|
-
throw new Error(
|
|
5095
|
+
throw new Error('start_playtest is single-player only. Use multiplayer_playtest action="start" for multi-client StudioTestService sessions.');
|
|
4546
5096
|
}
|
|
4547
5097
|
const data = { mode };
|
|
4548
5098
|
const startedAt = Date.now();
|
|
@@ -4581,7 +5131,7 @@ ${code}`
|
|
|
4581
5131
|
let wait;
|
|
4582
5132
|
if (response?.success === true) {
|
|
4583
5133
|
const requiredRoles = mode === "play" ? ["server", "client-1"] : ["server"];
|
|
4584
|
-
wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles,
|
|
5134
|
+
wait = await this._waitForRuntimeRolesFresh(resolved.targetInstanceId, startedAt, requiredRoles, timeout, true);
|
|
4585
5135
|
}
|
|
4586
5136
|
const body = wait ? {
|
|
4587
5137
|
...response,
|
|
@@ -4598,7 +5148,7 @@ ${code}`
|
|
|
4598
5148
|
]
|
|
4599
5149
|
};
|
|
4600
5150
|
}
|
|
4601
|
-
async stopPlaytest(instance_id) {
|
|
5151
|
+
async stopPlaytest(instance_id, timeout = 15) {
|
|
4602
5152
|
const { instanceId } = this._resolveSingleTarget("edit", instance_id);
|
|
4603
5153
|
let response;
|
|
4604
5154
|
let stopRequestError;
|
|
@@ -4614,7 +5164,7 @@ ${code}`
|
|
|
4614
5164
|
}
|
|
4615
5165
|
let wait;
|
|
4616
5166
|
if (response?.success === true) {
|
|
4617
|
-
wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true },
|
|
5167
|
+
wait = await this._waitForRuntimeRoles(instanceId, { noRuntime: true }, timeout, true);
|
|
4618
5168
|
} else if (this._runtimeTargetsForEquivalentInstances(instanceId).length > 0) {
|
|
4619
5169
|
wait = {
|
|
4620
5170
|
ok: false,
|
|
@@ -4739,6 +5289,94 @@ ${code}`
|
|
|
4739
5289
|
}
|
|
4740
5290
|
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
|
|
4741
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
|
+
}
|
|
4742
5380
|
async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id) {
|
|
4743
5381
|
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
4744
5382
|
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
@@ -4876,14 +5514,14 @@ ${code}`
|
|
|
4876
5514
|
};
|
|
4877
5515
|
}
|
|
4878
5516
|
static findProjectRoot(startDir) {
|
|
4879
|
-
let dir =
|
|
5517
|
+
let dir = path2.resolve(startDir);
|
|
4880
5518
|
let previous = "";
|
|
4881
5519
|
while (dir !== previous) {
|
|
4882
|
-
if (fs.existsSync(
|
|
5520
|
+
if (fs.existsSync(path2.join(dir, ".git")) || fs.existsSync(path2.join(dir, "package.json"))) {
|
|
4883
5521
|
return dir;
|
|
4884
5522
|
}
|
|
4885
5523
|
previous = dir;
|
|
4886
|
-
dir =
|
|
5524
|
+
dir = path2.dirname(dir);
|
|
4887
5525
|
}
|
|
4888
5526
|
return null;
|
|
4889
5527
|
}
|
|
@@ -4897,7 +5535,7 @@ ${code}`
|
|
|
4897
5535
|
}
|
|
4898
5536
|
}
|
|
4899
5537
|
static ensureWritableDirectory(candidate, label) {
|
|
4900
|
-
const resolved =
|
|
5538
|
+
const resolved = path2.resolve(candidate);
|
|
4901
5539
|
try {
|
|
4902
5540
|
fs.mkdirSync(resolved, { recursive: true });
|
|
4903
5541
|
} catch (error) {
|
|
@@ -4918,11 +5556,11 @@ ${code}`
|
|
|
4918
5556
|
if (_RobloxStudioTools._cachedLibraryPath)
|
|
4919
5557
|
return _RobloxStudioTools._cachedLibraryPath;
|
|
4920
5558
|
const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
|
|
4921
|
-
const cwd =
|
|
5559
|
+
const cwd = path2.resolve(process.cwd());
|
|
4922
5560
|
const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
|
|
4923
|
-
const homeLibraryPath =
|
|
4924
|
-
const projectLibraryPath = projectRoot ?
|
|
4925
|
-
const cwdLibraryPath =
|
|
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");
|
|
4926
5564
|
let result;
|
|
4927
5565
|
if (overridePath) {
|
|
4928
5566
|
result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
|
|
@@ -4936,7 +5574,7 @@ ${code}`
|
|
|
4936
5574
|
}
|
|
4937
5575
|
})());
|
|
4938
5576
|
if (existing) {
|
|
4939
|
-
result =
|
|
5577
|
+
result = path2.resolve(existing);
|
|
4940
5578
|
} else if (projectLibraryPath) {
|
|
4941
5579
|
try {
|
|
4942
5580
|
result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
|
|
@@ -4963,8 +5601,8 @@ ${code}`
|
|
|
4963
5601
|
if (response && response.success && response.buildData) {
|
|
4964
5602
|
const buildData = response.buildData;
|
|
4965
5603
|
const buildId = buildData.id || `${style}/exported`;
|
|
4966
|
-
const filePath =
|
|
4967
|
-
const dirPath =
|
|
5604
|
+
const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
|
|
5605
|
+
const dirPath = path2.dirname(filePath);
|
|
4968
5606
|
if (!fs.existsSync(dirPath)) {
|
|
4969
5607
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
4970
5608
|
}
|
|
@@ -5065,8 +5703,8 @@ ${code}`
|
|
|
5065
5703
|
const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
|
|
5066
5704
|
const computedBounds = bounds || this.computeBounds(normalizedParts);
|
|
5067
5705
|
const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
|
|
5068
|
-
const filePath =
|
|
5069
|
-
const dirPath =
|
|
5706
|
+
const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
5707
|
+
const dirPath = path2.dirname(filePath);
|
|
5070
5708
|
if (!fs.existsSync(dirPath)) {
|
|
5071
5709
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
5072
5710
|
}
|
|
@@ -5124,8 +5762,8 @@ ${code}`
|
|
|
5124
5762
|
};
|
|
5125
5763
|
if (seed !== void 0)
|
|
5126
5764
|
buildData.generatorSeed = seed;
|
|
5127
|
-
const filePath =
|
|
5128
|
-
const dirPath =
|
|
5765
|
+
const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
5766
|
+
const dirPath = path2.dirname(filePath);
|
|
5129
5767
|
if (!fs.existsSync(dirPath)) {
|
|
5130
5768
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
5131
5769
|
}
|
|
@@ -5153,13 +5791,13 @@ ${code}`
|
|
|
5153
5791
|
}
|
|
5154
5792
|
let resolved;
|
|
5155
5793
|
if (typeof buildData === "string") {
|
|
5156
|
-
const filePath =
|
|
5794
|
+
const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
|
|
5157
5795
|
if (!fs.existsSync(filePath)) {
|
|
5158
5796
|
throw new Error(`Build not found in library: ${buildData}`);
|
|
5159
5797
|
}
|
|
5160
5798
|
resolved = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5161
5799
|
} else if (buildData.id && !buildData.parts) {
|
|
5162
|
-
const filePath =
|
|
5800
|
+
const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
|
|
5163
5801
|
if (!fs.existsSync(filePath)) {
|
|
5164
5802
|
throw new Error(`Build not found in library: ${buildData.id}`);
|
|
5165
5803
|
}
|
|
@@ -5186,13 +5824,13 @@ ${code}`
|
|
|
5186
5824
|
const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
|
|
5187
5825
|
const builds = [];
|
|
5188
5826
|
for (const s of styles) {
|
|
5189
|
-
const dirPath =
|
|
5827
|
+
const dirPath = path2.join(libraryPath, s);
|
|
5190
5828
|
if (!fs.existsSync(dirPath))
|
|
5191
5829
|
continue;
|
|
5192
5830
|
const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
|
|
5193
5831
|
for (const file of files) {
|
|
5194
5832
|
try {
|
|
5195
|
-
const content = fs.readFileSync(
|
|
5833
|
+
const content = fs.readFileSync(path2.join(dirPath, file), "utf-8");
|
|
5196
5834
|
const data = JSON.parse(content);
|
|
5197
5835
|
builds.push({
|
|
5198
5836
|
id: data.id || `${s}/${file.replace(".json", "")}`,
|
|
@@ -5231,7 +5869,7 @@ ${code}`
|
|
|
5231
5869
|
if (!id) {
|
|
5232
5870
|
throw new Error("Build ID is required for get_build");
|
|
5233
5871
|
}
|
|
5234
|
-
const filePath =
|
|
5872
|
+
const filePath = path2.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
5235
5873
|
if (!fs.existsSync(filePath)) {
|
|
5236
5874
|
throw new Error(`Build not found in library: ${id}`);
|
|
5237
5875
|
}
|
|
@@ -5318,7 +5956,7 @@ ${code}`
|
|
|
5318
5956
|
if (!buildId) {
|
|
5319
5957
|
throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
|
|
5320
5958
|
}
|
|
5321
|
-
const filePath =
|
|
5959
|
+
const filePath = path2.join(libraryPath, `${buildId}.json`);
|
|
5322
5960
|
if (!fs.existsSync(filePath)) {
|
|
5323
5961
|
throw new Error(`Build not found in library: ${buildId}`);
|
|
5324
5962
|
}
|
|
@@ -5501,7 +6139,7 @@ ${code}`
|
|
|
5501
6139
|
throw new Error(`File not found: ${filePath}`);
|
|
5502
6140
|
}
|
|
5503
6141
|
const fileContent = fs.readFileSync(filePath);
|
|
5504
|
-
const fileName =
|
|
6142
|
+
const fileName = path2.basename(filePath);
|
|
5505
6143
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
5506
6144
|
const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
|
|
5507
6145
|
return {
|
|
@@ -5598,23 +6236,6 @@ ${code}`
|
|
|
5598
6236
|
}]
|
|
5599
6237
|
};
|
|
5600
6238
|
}
|
|
5601
|
-
async characterNavigation(position, instancePath, waitForCompletion, timeout, target, instance_id) {
|
|
5602
|
-
if (!position && !instancePath) {
|
|
5603
|
-
throw new Error("Either position or instancePath is required for character_navigation");
|
|
5604
|
-
}
|
|
5605
|
-
const response = await this._callSingle("/api/character-navigation", {
|
|
5606
|
-
position,
|
|
5607
|
-
instancePath,
|
|
5608
|
-
waitForCompletion,
|
|
5609
|
-
timeout
|
|
5610
|
-
}, target || "edit", instance_id);
|
|
5611
|
-
return {
|
|
5612
|
-
content: [{
|
|
5613
|
-
type: "text",
|
|
5614
|
-
text: JSON.stringify(response)
|
|
5615
|
-
}]
|
|
5616
|
-
};
|
|
5617
|
-
}
|
|
5618
6239
|
async cloneObject(instancePath, targetParentPath, instance_id) {
|
|
5619
6240
|
if (!instancePath || !targetParentPath) {
|
|
5620
6241
|
throw new Error("instancePath and targetParentPath are required for clone_object");
|
|
@@ -5743,9 +6364,9 @@ ${code}`
|
|
|
5743
6364
|
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
5744
6365
|
}
|
|
5745
6366
|
const bytes = Buffer.from(response.base64, "base64");
|
|
5746
|
-
const resolved =
|
|
6367
|
+
const resolved = path2.resolve(outputPath);
|
|
5747
6368
|
try {
|
|
5748
|
-
fs.mkdirSync(
|
|
6369
|
+
fs.mkdirSync(path2.dirname(resolved), { recursive: true });
|
|
5749
6370
|
fs.writeFileSync(resolved, bytes);
|
|
5750
6371
|
} catch (err) {
|
|
5751
6372
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
|
|
@@ -5779,7 +6400,7 @@ ${code}`
|
|
|
5779
6400
|
let bytes;
|
|
5780
6401
|
let sourceLabel;
|
|
5781
6402
|
if (source.path !== void 0) {
|
|
5782
|
-
const resolved =
|
|
6403
|
+
const resolved = path2.resolve(source.path);
|
|
5783
6404
|
try {
|
|
5784
6405
|
bytes = fs.readFileSync(resolved);
|
|
5785
6406
|
} catch (err) {
|
|
@@ -5848,7 +6469,7 @@ ${code}`
|
|
|
5848
6469
|
if (response.error) {
|
|
5849
6470
|
let text = response.error;
|
|
5850
6471
|
if (targetRole.startsWith("client-") && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
|
|
5851
|
-
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
|
|
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}`;
|
|
5852
6473
|
}
|
|
5853
6474
|
return { success: false, error: text };
|
|
5854
6475
|
}
|
|
@@ -6021,7 +6642,7 @@ var init_server = __esm({
|
|
|
6021
6642
|
config;
|
|
6022
6643
|
constructor(config) {
|
|
6023
6644
|
this.config = config;
|
|
6024
|
-
this.allowedToolNames = new Set(config.tools.map((t) => t.name));
|
|
6645
|
+
this.allowedToolNames = new Set((config.callableTools ?? config.tools).map((t) => t.name));
|
|
6025
6646
|
this.server = new Server2({
|
|
6026
6647
|
name: config.name,
|
|
6027
6648
|
version: config.version
|
|
@@ -6194,7 +6815,7 @@ var init_server = __esm({
|
|
|
6194
6815
|
});
|
|
6195
6816
|
|
|
6196
6817
|
// ../core/dist/tools/definitions.js
|
|
6197
|
-
var TOOL_DEFINITIONS, getAllTools;
|
|
6818
|
+
var TOOL_DEFINITIONS, DEPRECATED_TOOL_DEFINITIONS, getAllTools, getAllCallableTools;
|
|
6198
6819
|
var init_definitions = __esm({
|
|
6199
6820
|
"../core/dist/tools/definitions.js"() {
|
|
6200
6821
|
"use strict";
|
|
@@ -6209,7 +6830,7 @@ var init_definitions = __esm({
|
|
|
6209
6830
|
properties: {
|
|
6210
6831
|
path: {
|
|
6211
6832
|
type: "string",
|
|
6212
|
-
description:
|
|
6833
|
+
description: 'Canonical DataModel path (default: game root), such as game.Workspace or game.ServerScriptService[".dir"]'
|
|
6213
6834
|
},
|
|
6214
6835
|
instance_id: {
|
|
6215
6836
|
type: "string",
|
|
@@ -6313,7 +6934,7 @@ var init_definitions = __esm({
|
|
|
6313
6934
|
properties: {
|
|
6314
6935
|
instancePath: {
|
|
6315
6936
|
type: "string",
|
|
6316
|
-
description:
|
|
6937
|
+
description: 'Canonical DataModel path, such as game.Workspace.Part or game.ServerScriptService[".dir"].Main'
|
|
6317
6938
|
},
|
|
6318
6939
|
excludeSource: {
|
|
6319
6940
|
type: "boolean",
|
|
@@ -6336,7 +6957,7 @@ var init_definitions = __esm({
|
|
|
6336
6957
|
properties: {
|
|
6337
6958
|
instancePath: {
|
|
6338
6959
|
type: "string",
|
|
6339
|
-
description:
|
|
6960
|
+
description: 'Canonical DataModel path, such as game.Workspace.Part or game.ServerScriptService[".dir"].Main'
|
|
6340
6961
|
},
|
|
6341
6962
|
instance_id: {
|
|
6342
6963
|
type: "string",
|
|
@@ -6398,7 +7019,7 @@ var init_definitions = __esm({
|
|
|
6398
7019
|
properties: {
|
|
6399
7020
|
path: {
|
|
6400
7021
|
type: "string",
|
|
6401
|
-
description: "
|
|
7022
|
+
description: "Canonical DataModel path (default: workspace root)"
|
|
6402
7023
|
},
|
|
6403
7024
|
maxDepth: {
|
|
6404
7025
|
type: "number",
|
|
@@ -6425,7 +7046,7 @@ var init_definitions = __esm({
|
|
|
6425
7046
|
properties: {
|
|
6426
7047
|
instancePath: {
|
|
6427
7048
|
type: "string",
|
|
6428
|
-
description: "
|
|
7049
|
+
description: "Canonical DataModel path"
|
|
6429
7050
|
},
|
|
6430
7051
|
propertyName: {
|
|
6431
7052
|
type: "string",
|
|
@@ -6452,7 +7073,7 @@ var init_definitions = __esm({
|
|
|
6452
7073
|
paths: {
|
|
6453
7074
|
type: "array",
|
|
6454
7075
|
items: { type: "string" },
|
|
6455
|
-
description: "
|
|
7076
|
+
description: "Canonical DataModel paths"
|
|
6456
7077
|
},
|
|
6457
7078
|
propertyName: {
|
|
6458
7079
|
type: "string",
|
|
@@ -6479,7 +7100,7 @@ var init_definitions = __esm({
|
|
|
6479
7100
|
paths: {
|
|
6480
7101
|
type: "array",
|
|
6481
7102
|
items: { type: "string" },
|
|
6482
|
-
description: "
|
|
7103
|
+
description: "Canonical DataModel paths"
|
|
6483
7104
|
},
|
|
6484
7105
|
propertyName: {
|
|
6485
7106
|
type: "string",
|
|
@@ -6502,7 +7123,7 @@ var init_definitions = __esm({
|
|
|
6502
7123
|
properties: {
|
|
6503
7124
|
instancePath: {
|
|
6504
7125
|
type: "string",
|
|
6505
|
-
description: "
|
|
7126
|
+
description: "Canonical DataModel path"
|
|
6506
7127
|
},
|
|
6507
7128
|
properties: {
|
|
6508
7129
|
type: "object",
|
|
@@ -6530,7 +7151,7 @@ var init_definitions = __esm({
|
|
|
6530
7151
|
},
|
|
6531
7152
|
parent: {
|
|
6532
7153
|
type: "string",
|
|
6533
|
-
description: "
|
|
7154
|
+
description: "Canonical parent DataModel path"
|
|
6534
7155
|
},
|
|
6535
7156
|
name: {
|
|
6536
7157
|
type: "string",
|
|
@@ -6566,7 +7187,7 @@ var init_definitions = __esm({
|
|
|
6566
7187
|
},
|
|
6567
7188
|
parent: {
|
|
6568
7189
|
type: "string",
|
|
6569
|
-
description: "
|
|
7190
|
+
description: "Canonical parent DataModel path"
|
|
6570
7191
|
},
|
|
6571
7192
|
name: {
|
|
6572
7193
|
type: "string",
|
|
@@ -6598,7 +7219,7 @@ var init_definitions = __esm({
|
|
|
6598
7219
|
properties: {
|
|
6599
7220
|
instancePath: {
|
|
6600
7221
|
type: "string",
|
|
6601
|
-
description: "
|
|
7222
|
+
description: "Canonical DataModel path"
|
|
6602
7223
|
},
|
|
6603
7224
|
instance_id: {
|
|
6604
7225
|
type: "string",
|
|
@@ -6618,7 +7239,7 @@ var init_definitions = __esm({
|
|
|
6618
7239
|
properties: {
|
|
6619
7240
|
instancePath: {
|
|
6620
7241
|
type: "string",
|
|
6621
|
-
description: "
|
|
7242
|
+
description: "Canonical DataModel path"
|
|
6622
7243
|
},
|
|
6623
7244
|
count: {
|
|
6624
7245
|
type: "number",
|
|
@@ -6679,7 +7300,7 @@ var init_definitions = __esm({
|
|
|
6679
7300
|
properties: {
|
|
6680
7301
|
instancePath: {
|
|
6681
7302
|
type: "string",
|
|
6682
|
-
description: "
|
|
7303
|
+
description: "Canonical DataModel path"
|
|
6683
7304
|
},
|
|
6684
7305
|
count: {
|
|
6685
7306
|
type: "number",
|
|
@@ -6742,7 +7363,7 @@ var init_definitions = __esm({
|
|
|
6742
7363
|
properties: {
|
|
6743
7364
|
instancePath: {
|
|
6744
7365
|
type: "string",
|
|
6745
|
-
description: "
|
|
7366
|
+
description: "Canonical path to a LuaSourceContainer"
|
|
6746
7367
|
},
|
|
6747
7368
|
startLine: {
|
|
6748
7369
|
type: "number",
|
|
@@ -6769,7 +7390,7 @@ var init_definitions = __esm({
|
|
|
6769
7390
|
properties: {
|
|
6770
7391
|
instancePath: {
|
|
6771
7392
|
type: "string",
|
|
6772
|
-
description: "
|
|
7393
|
+
description: "Canonical path to a LuaSourceContainer"
|
|
6773
7394
|
},
|
|
6774
7395
|
source: {
|
|
6775
7396
|
type: "string",
|
|
@@ -6792,7 +7413,7 @@ var init_definitions = __esm({
|
|
|
6792
7413
|
properties: {
|
|
6793
7414
|
instancePath: {
|
|
6794
7415
|
type: "string",
|
|
6795
|
-
description: "
|
|
7416
|
+
description: "Canonical path to a LuaSourceContainer"
|
|
6796
7417
|
},
|
|
6797
7418
|
old_string: {
|
|
6798
7419
|
type: "string",
|
|
@@ -6823,7 +7444,7 @@ var init_definitions = __esm({
|
|
|
6823
7444
|
properties: {
|
|
6824
7445
|
instancePath: {
|
|
6825
7446
|
type: "string",
|
|
6826
|
-
description: "
|
|
7447
|
+
description: "Canonical path to a LuaSourceContainer"
|
|
6827
7448
|
},
|
|
6828
7449
|
afterLine: {
|
|
6829
7450
|
type: "number",
|
|
@@ -6850,7 +7471,7 @@ var init_definitions = __esm({
|
|
|
6850
7471
|
properties: {
|
|
6851
7472
|
instancePath: {
|
|
6852
7473
|
type: "string",
|
|
6853
|
-
description: "
|
|
7474
|
+
description: "Canonical path to a LuaSourceContainer"
|
|
6854
7475
|
},
|
|
6855
7476
|
startLine: {
|
|
6856
7477
|
type: "number",
|
|
@@ -6878,7 +7499,7 @@ var init_definitions = __esm({
|
|
|
6878
7499
|
properties: {
|
|
6879
7500
|
instancePath: {
|
|
6880
7501
|
type: "string",
|
|
6881
|
-
description: "
|
|
7502
|
+
description: "Canonical DataModel path"
|
|
6882
7503
|
},
|
|
6883
7504
|
attributeName: {
|
|
6884
7505
|
type: "string",
|
|
@@ -6908,7 +7529,7 @@ var init_definitions = __esm({
|
|
|
6908
7529
|
properties: {
|
|
6909
7530
|
instancePath: {
|
|
6910
7531
|
type: "string",
|
|
6911
|
-
description: "
|
|
7532
|
+
description: "Canonical DataModel path"
|
|
6912
7533
|
},
|
|
6913
7534
|
instance_id: {
|
|
6914
7535
|
type: "string",
|
|
@@ -6927,7 +7548,7 @@ var init_definitions = __esm({
|
|
|
6927
7548
|
properties: {
|
|
6928
7549
|
instancePath: {
|
|
6929
7550
|
type: "string",
|
|
6930
|
-
description: "
|
|
7551
|
+
description: "Canonical DataModel path"
|
|
6931
7552
|
},
|
|
6932
7553
|
attributeName: {
|
|
6933
7554
|
type: "string",
|
|
@@ -6951,7 +7572,7 @@ var init_definitions = __esm({
|
|
|
6951
7572
|
properties: {
|
|
6952
7573
|
instancePath: {
|
|
6953
7574
|
type: "string",
|
|
6954
|
-
description: "
|
|
7575
|
+
description: "Canonical DataModel path"
|
|
6955
7576
|
},
|
|
6956
7577
|
instance_id: {
|
|
6957
7578
|
type: "string",
|
|
@@ -6970,7 +7591,7 @@ var init_definitions = __esm({
|
|
|
6970
7591
|
properties: {
|
|
6971
7592
|
instancePath: {
|
|
6972
7593
|
type: "string",
|
|
6973
|
-
description: "
|
|
7594
|
+
description: "Canonical DataModel path"
|
|
6974
7595
|
},
|
|
6975
7596
|
tagName: {
|
|
6976
7597
|
type: "string",
|
|
@@ -6993,7 +7614,7 @@ var init_definitions = __esm({
|
|
|
6993
7614
|
properties: {
|
|
6994
7615
|
instancePath: {
|
|
6995
7616
|
type: "string",
|
|
6996
|
-
description: "
|
|
7617
|
+
description: "Canonical DataModel path"
|
|
6997
7618
|
},
|
|
6998
7619
|
tagName: {
|
|
6999
7620
|
type: "string",
|
|
@@ -7160,43 +7781,92 @@ var init_definitions = __esm({
|
|
|
7160
7781
|
required: ["pattern"]
|
|
7161
7782
|
}
|
|
7162
7783
|
},
|
|
7163
|
-
// ===
|
|
7784
|
+
// === Studio Instance Management ===
|
|
7164
7785
|
{
|
|
7165
|
-
name: "
|
|
7786
|
+
name: "manage_instance",
|
|
7166
7787
|
category: "write",
|
|
7167
|
-
description:
|
|
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.',
|
|
7168
7789
|
inputSchema: {
|
|
7169
7790
|
type: "object",
|
|
7170
7791
|
properties: {
|
|
7171
|
-
|
|
7792
|
+
action: {
|
|
7172
7793
|
type: "string",
|
|
7173
|
-
enum: ["
|
|
7174
|
-
description: "
|
|
7794
|
+
enum: ["launch", "close", "status", "list_place_versions"],
|
|
7795
|
+
description: "Instance management action."
|
|
7175
7796
|
},
|
|
7176
|
-
|
|
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: {
|
|
7177
7811
|
type: "number",
|
|
7178
|
-
description: "
|
|
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: {
|
|
7823
|
+
type: "number",
|
|
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.'
|
|
7179
7833
|
},
|
|
7180
7834
|
instance_id: {
|
|
7181
7835
|
type: "string",
|
|
7182
|
-
description: "
|
|
7836
|
+
description: 'For action="close" or action="status": Studio instance to inspect or close. close accepts MCP-managed instances and explicitly connected edit instances.'
|
|
7183
7837
|
}
|
|
7184
7838
|
},
|
|
7185
|
-
required: ["
|
|
7839
|
+
required: ["action"]
|
|
7186
7840
|
}
|
|
7187
7841
|
},
|
|
7842
|
+
// === Playtest ===
|
|
7188
7843
|
{
|
|
7189
|
-
name: "
|
|
7844
|
+
name: "solo_playtest",
|
|
7190
7845
|
category: "write",
|
|
7191
|
-
description: "
|
|
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.',
|
|
7192
7847
|
inputSchema: {
|
|
7193
7848
|
type: "object",
|
|
7194
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
|
+
},
|
|
7195
7864
|
instance_id: {
|
|
7196
7865
|
type: "string",
|
|
7197
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."
|
|
7198
7867
|
}
|
|
7199
|
-
}
|
|
7868
|
+
},
|
|
7869
|
+
required: ["action"]
|
|
7200
7870
|
}
|
|
7201
7871
|
},
|
|
7202
7872
|
{
|
|
@@ -7265,7 +7935,7 @@ var init_definitions = __esm({
|
|
|
7265
7935
|
{
|
|
7266
7936
|
name: "get_simulation_state",
|
|
7267
7937
|
category: "read",
|
|
7268
|
-
description: 'Inspect current NetworkSettings and/or StudioDeviceSimulatorService state for edit and connected
|
|
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.',
|
|
7269
7939
|
inputSchema: {
|
|
7270
7940
|
type: "object",
|
|
7271
7941
|
properties: {
|
|
@@ -7288,7 +7958,7 @@ var init_definitions = __esm({
|
|
|
7288
7958
|
{
|
|
7289
7959
|
name: "reset_simulation_state",
|
|
7290
7960
|
category: "write",
|
|
7291
|
-
description: 'Reset reachable
|
|
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.',
|
|
7292
7962
|
inputSchema: {
|
|
7293
7963
|
type: "object",
|
|
7294
7964
|
properties: {
|
|
@@ -7474,109 +8144,41 @@ var init_definitions = __esm({
|
|
|
7474
8144
|
}
|
|
7475
8145
|
},
|
|
7476
8146
|
{
|
|
7477
|
-
name: "
|
|
8147
|
+
name: "multiplayer_playtest",
|
|
7478
8148
|
category: "write",
|
|
7479
|
-
description:
|
|
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.',
|
|
7480
8150
|
inputSchema: {
|
|
7481
8151
|
type: "object",
|
|
7482
8152
|
properties: {
|
|
7483
|
-
|
|
7484
|
-
type: "number",
|
|
7485
|
-
description: "Number of client players to start (1-8)."
|
|
7486
|
-
},
|
|
7487
|
-
testArgs: {
|
|
7488
|
-
description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
|
|
7489
|
-
},
|
|
7490
|
-
timeout: {
|
|
7491
|
-
type: "number",
|
|
7492
|
-
description: "Max seconds to wait for server + clients to register (default 30)."
|
|
7493
|
-
},
|
|
7494
|
-
instance_id: {
|
|
7495
|
-
type: "string",
|
|
7496
|
-
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
7497
|
-
}
|
|
7498
|
-
},
|
|
7499
|
-
required: ["numPlayers"]
|
|
7500
|
-
}
|
|
7501
|
-
},
|
|
7502
|
-
{
|
|
7503
|
-
name: "multiplayer_test_state",
|
|
7504
|
-
category: "read",
|
|
7505
|
-
description: "Get the active multiplayer StudioTestService state for a place: phase, peers, players, original testArgs, result/error, and connected client roles.",
|
|
7506
|
-
inputSchema: {
|
|
7507
|
-
type: "object",
|
|
7508
|
-
properties: {
|
|
7509
|
-
instance_id: {
|
|
8153
|
+
action: {
|
|
7510
8154
|
type: "string",
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
}
|
|
7514
|
-
}
|
|
7515
|
-
},
|
|
7516
|
-
{
|
|
7517
|
-
name: "multiplayer_test_add_players",
|
|
7518
|
-
category: "write",
|
|
7519
|
-
description: "Add client players to a running StudioTestService multiplayer test and wait for the new clients to connect.",
|
|
7520
|
-
inputSchema: {
|
|
7521
|
-
type: "object",
|
|
7522
|
-
properties: {
|
|
7523
|
-
numPlayers: {
|
|
7524
|
-
type: "number",
|
|
7525
|
-
description: "Number of additional client players to add (1-8)."
|
|
8155
|
+
enum: ["start", "status", "add_players", "leave_client", "end"],
|
|
8156
|
+
description: "Lifecycle action to run."
|
|
7526
8157
|
},
|
|
7527
|
-
|
|
8158
|
+
numPlayers: {
|
|
7528
8159
|
type: "number",
|
|
7529
|
-
description:
|
|
8160
|
+
description: 'Required for action="start" and action="add_players". Number of client players (1-8).'
|
|
7530
8161
|
},
|
|
7531
|
-
instance_id: {
|
|
7532
|
-
type: "string",
|
|
7533
|
-
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
7534
|
-
}
|
|
7535
|
-
},
|
|
7536
|
-
required: ["numPlayers"]
|
|
7537
|
-
}
|
|
7538
|
-
},
|
|
7539
|
-
{
|
|
7540
|
-
name: "multiplayer_test_leave_client",
|
|
7541
|
-
category: "write",
|
|
7542
|
-
description: "Disconnect a specific client from a running StudioTestService multiplayer test, then wait for that client peer to leave.",
|
|
7543
|
-
inputSchema: {
|
|
7544
|
-
type: "object",
|
|
7545
|
-
properties: {
|
|
7546
8162
|
target: {
|
|
7547
8163
|
type: "string",
|
|
7548
|
-
description: 'Client target
|
|
8164
|
+
description: 'Client target for action="leave_client", such as "client-1". Defaults to "client-1".'
|
|
7549
8165
|
},
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
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.'
|
|
7553
8168
|
},
|
|
7554
|
-
instance_id: {
|
|
7555
|
-
type: "string",
|
|
7556
|
-
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
7557
|
-
}
|
|
7558
|
-
}
|
|
7559
|
-
}
|
|
7560
|
-
},
|
|
7561
|
-
{
|
|
7562
|
-
name: "multiplayer_test_end",
|
|
7563
|
-
category: "write",
|
|
7564
|
-
description: "End a running StudioTestService multiplayer test with an optional return value, then wait for all runtime peers to disconnect.",
|
|
7565
|
-
inputSchema: {
|
|
7566
|
-
type: "object",
|
|
7567
|
-
properties: {
|
|
7568
8169
|
value: {
|
|
7569
|
-
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.'
|
|
7570
8171
|
},
|
|
7571
8172
|
timeout: {
|
|
7572
8173
|
type: "number",
|
|
7573
|
-
description: "Max seconds to wait for
|
|
8174
|
+
description: "Max seconds to wait for action completion. Defaults to 30."
|
|
7574
8175
|
},
|
|
7575
8176
|
instance_id: {
|
|
7576
8177
|
type: "string",
|
|
7577
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."
|
|
7578
8179
|
}
|
|
7579
|
-
}
|
|
8180
|
+
},
|
|
8181
|
+
required: ["action"]
|
|
7580
8182
|
}
|
|
7581
8183
|
},
|
|
7582
8184
|
{
|
|
@@ -7689,7 +8291,7 @@ var init_definitions = __esm({
|
|
|
7689
8291
|
},
|
|
7690
8292
|
script_path: {
|
|
7691
8293
|
type: "string",
|
|
7692
|
-
description:
|
|
8294
|
+
description: 'Canonical path to a LuaSourceContainer, for example game.ServerScriptService.Main or game.ServerScriptService[".dir"].ReproScript. Required for set/remove.'
|
|
7693
8295
|
},
|
|
7694
8296
|
line: {
|
|
7695
8297
|
type: "number",
|
|
@@ -7772,7 +8374,7 @@ var init_definitions = __esm({
|
|
|
7772
8374
|
properties: {
|
|
7773
8375
|
instancePath: {
|
|
7774
8376
|
type: "string",
|
|
7775
|
-
description: "
|
|
8377
|
+
description: "Canonical path to the Model or Folder to export"
|
|
7776
8378
|
},
|
|
7777
8379
|
outputId: {
|
|
7778
8380
|
type: "string",
|
|
@@ -7929,7 +8531,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
7929
8531
|
},
|
|
7930
8532
|
targetPath: {
|
|
7931
8533
|
type: "string",
|
|
7932
|
-
description: "
|
|
8534
|
+
description: "Canonical parent DataModel path where the model will be created"
|
|
7933
8535
|
},
|
|
7934
8536
|
position: {
|
|
7935
8537
|
type: "array",
|
|
@@ -8060,7 +8662,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8060
8662
|
},
|
|
8061
8663
|
targetPath: {
|
|
8062
8664
|
type: "string",
|
|
8063
|
-
description: "
|
|
8665
|
+
description: "Canonical parent DataModel path for the scene (default: game.Workspace)"
|
|
8064
8666
|
},
|
|
8065
8667
|
instance_id: {
|
|
8066
8668
|
type: "string",
|
|
@@ -8152,7 +8754,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8152
8754
|
},
|
|
8153
8755
|
parentPath: {
|
|
8154
8756
|
type: "string",
|
|
8155
|
-
description: "
|
|
8757
|
+
description: "Canonical parent DataModel path (default: game.Workspace)"
|
|
8156
8758
|
},
|
|
8157
8759
|
position: {
|
|
8158
8760
|
type: "object",
|
|
@@ -8330,42 +8932,6 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8330
8932
|
}
|
|
8331
8933
|
}
|
|
8332
8934
|
},
|
|
8333
|
-
// === Character Navigation ===
|
|
8334
|
-
{
|
|
8335
|
-
name: "character_navigation",
|
|
8336
|
-
category: "write",
|
|
8337
|
-
description: 'Move the player character to a target position or instance during playtest. Uses PathfindingService for automatic navigation around obstacles, falling back to direct movement. Requires an active playtest in "play" mode. Does NOT simulate player input - moves the character directly.',
|
|
8338
|
-
inputSchema: {
|
|
8339
|
-
type: "object",
|
|
8340
|
-
properties: {
|
|
8341
|
-
position: {
|
|
8342
|
-
type: "array",
|
|
8343
|
-
items: { type: "number" },
|
|
8344
|
-
description: "Target world position [x, y, z]. Either this or instancePath is required."
|
|
8345
|
-
},
|
|
8346
|
-
instancePath: {
|
|
8347
|
-
type: "string",
|
|
8348
|
-
description: "Instance to navigate to (dot notation). The character walks to its Position. Either this or position is required."
|
|
8349
|
-
},
|
|
8350
|
-
waitForCompletion: {
|
|
8351
|
-
type: "boolean",
|
|
8352
|
-
description: "Wait for the character to arrive before returning (default: true)"
|
|
8353
|
-
},
|
|
8354
|
-
timeout: {
|
|
8355
|
-
type: "number",
|
|
8356
|
-
description: "Max seconds to wait for navigation to complete (default: 25)"
|
|
8357
|
-
},
|
|
8358
|
-
target: {
|
|
8359
|
-
type: "string",
|
|
8360
|
-
description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
|
|
8361
|
-
},
|
|
8362
|
-
instance_id: {
|
|
8363
|
-
type: "string",
|
|
8364
|
-
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
8365
|
-
}
|
|
8366
|
-
}
|
|
8367
|
-
}
|
|
8368
|
-
},
|
|
8369
8935
|
// === Instance Operations ===
|
|
8370
8936
|
{
|
|
8371
8937
|
name: "clone_object",
|
|
@@ -8376,11 +8942,11 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8376
8942
|
properties: {
|
|
8377
8943
|
instancePath: {
|
|
8378
8944
|
type: "string",
|
|
8379
|
-
description: "
|
|
8945
|
+
description: "Canonical path of the instance to clone"
|
|
8380
8946
|
},
|
|
8381
8947
|
targetParentPath: {
|
|
8382
8948
|
type: "string",
|
|
8383
|
-
description: "
|
|
8949
|
+
description: "Canonical path of the parent to place the clone under"
|
|
8384
8950
|
},
|
|
8385
8951
|
instance_id: {
|
|
8386
8952
|
type: "string",
|
|
@@ -8400,7 +8966,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8400
8966
|
properties: {
|
|
8401
8967
|
instancePath: {
|
|
8402
8968
|
type: "string",
|
|
8403
|
-
description: "
|
|
8969
|
+
description: "Canonical root DataModel path"
|
|
8404
8970
|
},
|
|
8405
8971
|
maxDepth: {
|
|
8406
8972
|
type: "number",
|
|
@@ -8427,11 +8993,11 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8427
8993
|
properties: {
|
|
8428
8994
|
instancePathA: {
|
|
8429
8995
|
type: "string",
|
|
8430
|
-
description: "First
|
|
8996
|
+
description: "First canonical DataModel path"
|
|
8431
8997
|
},
|
|
8432
8998
|
instancePathB: {
|
|
8433
8999
|
type: "string",
|
|
8434
|
-
description: "Second
|
|
9000
|
+
description: "Second canonical DataModel path"
|
|
8435
9001
|
},
|
|
8436
9002
|
instance_id: {
|
|
8437
9003
|
type: "string",
|
|
@@ -8451,7 +9017,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8451
9017
|
properties: {
|
|
8452
9018
|
instancePath: {
|
|
8453
9019
|
type: "string",
|
|
8454
|
-
description: "
|
|
9020
|
+
description: "Canonical DataModel path"
|
|
8455
9021
|
},
|
|
8456
9022
|
attributes: {
|
|
8457
9023
|
type: "object",
|
|
@@ -8533,7 +9099,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8533
9099
|
instance_paths: {
|
|
8534
9100
|
type: "array",
|
|
8535
9101
|
items: { type: "string" },
|
|
8536
|
-
description: 'DataModel paths to serialize (e.g. ["Workspace.TestRig", "ServerStorage.Templates.NPC"])'
|
|
9102
|
+
description: 'Canonical DataModel paths to serialize (e.g. ["game.Workspace.TestRig", "game.ServerStorage.Templates.NPC"])'
|
|
8537
9103
|
},
|
|
8538
9104
|
output_path: {
|
|
8539
9105
|
type: "string",
|
|
@@ -8575,7 +9141,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8575
9141
|
},
|
|
8576
9142
|
parent_path: {
|
|
8577
9143
|
type: "string",
|
|
8578
|
-
description: 'DataModel path of the Instance to parent imported instances under (e.g. "ServerStorage.Imported")'
|
|
9144
|
+
description: 'Canonical DataModel path of the Instance to parent imported instances under (e.g. "game.ServerStorage.Imported")'
|
|
8579
9145
|
},
|
|
8580
9146
|
target: {
|
|
8581
9147
|
type: "string",
|
|
@@ -8640,20 +9206,168 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
8640
9206
|
}
|
|
8641
9207
|
}
|
|
8642
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
|
+
];
|
|
8643
9356
|
getAllTools = () => [...TOOL_DEFINITIONS];
|
|
9357
|
+
getAllCallableTools = () => [...TOOL_DEFINITIONS, ...DEPRECATED_TOOL_DEFINITIONS];
|
|
8644
9358
|
}
|
|
8645
9359
|
});
|
|
8646
9360
|
|
|
8647
9361
|
// ../core/dist/install-plugin-helpers.js
|
|
8648
|
-
import { existsSync as
|
|
9362
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync } from "fs";
|
|
8649
9363
|
import { execSync } from "child_process";
|
|
8650
|
-
import { join as
|
|
8651
|
-
import { homedir as
|
|
9364
|
+
import { join as join3 } from "path";
|
|
9365
|
+
import { homedir as homedir3 } from "os";
|
|
8652
9366
|
function isWSL() {
|
|
8653
9367
|
if (process.platform !== "linux")
|
|
8654
9368
|
return false;
|
|
8655
9369
|
try {
|
|
8656
|
-
const v =
|
|
9370
|
+
const v = readFileSync3("/proc/version", "utf8");
|
|
8657
9371
|
return /microsoft|wsl/i.test(v);
|
|
8658
9372
|
} catch {
|
|
8659
9373
|
return false;
|
|
@@ -8671,7 +9385,7 @@ function getWindowsUserPluginsDir() {
|
|
|
8671
9385
|
}).toString().trim();
|
|
8672
9386
|
if (!linuxPath)
|
|
8673
9387
|
return null;
|
|
8674
|
-
return
|
|
9388
|
+
return join3(linuxPath, "Roblox", "Plugins");
|
|
8675
9389
|
} catch {
|
|
8676
9390
|
return null;
|
|
8677
9391
|
}
|
|
@@ -8680,7 +9394,7 @@ function getPluginsFolder() {
|
|
|
8680
9394
|
if (process.env.MCP_PLUGINS_DIR)
|
|
8681
9395
|
return process.env.MCP_PLUGINS_DIR;
|
|
8682
9396
|
if (process.platform === "win32") {
|
|
8683
|
-
return
|
|
9397
|
+
return join3(process.env.LOCALAPPDATA || join3(homedir3(), "AppData", "Local"), "Roblox", "Plugins");
|
|
8684
9398
|
}
|
|
8685
9399
|
if (isWSL()) {
|
|
8686
9400
|
const win = getWindowsUserPluginsDir();
|
|
@@ -8688,11 +9402,11 @@ function getPluginsFolder() {
|
|
|
8688
9402
|
return win;
|
|
8689
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.");
|
|
8690
9404
|
}
|
|
8691
|
-
return
|
|
9405
|
+
return join3(homedir3(), "Documents", "Roblox", "Plugins");
|
|
8692
9406
|
}
|
|
8693
9407
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
8694
|
-
const otherDest =
|
|
8695
|
-
if (!
|
|
9408
|
+
const otherDest = join3(pluginsFolder, otherAssetName);
|
|
9409
|
+
if (!existsSync3(otherDest))
|
|
8696
9410
|
return;
|
|
8697
9411
|
if (replace) {
|
|
8698
9412
|
try {
|
|
@@ -8737,8 +9451,8 @@ __export(install_plugin_exports, {
|
|
|
8737
9451
|
installBundledPlugin: () => installBundledPlugin,
|
|
8738
9452
|
installPlugin: () => installPlugin
|
|
8739
9453
|
});
|
|
8740
|
-
import { copyFileSync, createWriteStream, existsSync as
|
|
8741
|
-
import { dirname as dirname2, join as
|
|
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";
|
|
8742
9456
|
import { fileURLToPath } from "url";
|
|
8743
9457
|
import { get } from "https";
|
|
8744
9458
|
function httpsGet(url) {
|
|
@@ -8808,7 +9522,7 @@ function prepareInstall({
|
|
|
8808
9522
|
warn
|
|
8809
9523
|
}) {
|
|
8810
9524
|
const pluginsFolder = getPluginsFolder();
|
|
8811
|
-
if (!
|
|
9525
|
+
if (!existsSync4(pluginsFolder)) {
|
|
8812
9526
|
mkdirSync2(pluginsFolder, { recursive: true });
|
|
8813
9527
|
}
|
|
8814
9528
|
handleVariantConflict({
|
|
@@ -8823,21 +9537,21 @@ function prepareInstall({
|
|
|
8823
9537
|
function bundledAssetPath() {
|
|
8824
9538
|
const currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
8825
9539
|
const candidates = [
|
|
8826
|
-
|
|
8827
|
-
|
|
9540
|
+
join4(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
9541
|
+
join4(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
8828
9542
|
];
|
|
8829
|
-
return candidates.find((candidate) =>
|
|
9543
|
+
return candidates.find((candidate) => existsSync4(candidate)) ?? null;
|
|
8830
9544
|
}
|
|
8831
9545
|
function packageVersion() {
|
|
8832
9546
|
const currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
8833
|
-
const pkg = JSON.parse(
|
|
9547
|
+
const pkg = JSON.parse(readFileSync4(join4(currentDir, "..", "package.json"), "utf8"));
|
|
8834
9548
|
if (!pkg.version) {
|
|
8835
9549
|
throw new Error("Package version not found");
|
|
8836
9550
|
}
|
|
8837
9551
|
return pkg.version;
|
|
8838
9552
|
}
|
|
8839
9553
|
function bundledPluginVersion(source) {
|
|
8840
|
-
const match =
|
|
9554
|
+
const match = readFileSync4(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
8841
9555
|
return match ? match[1] : null;
|
|
8842
9556
|
}
|
|
8843
9557
|
function assertBundledPluginVersion(source) {
|
|
@@ -8850,9 +9564,9 @@ function assertBundledPluginVersion(source) {
|
|
|
8850
9564
|
}
|
|
8851
9565
|
}
|
|
8852
9566
|
function filesMatch(a, b) {
|
|
8853
|
-
if (!
|
|
8854
|
-
const aBytes =
|
|
8855
|
-
const bBytes =
|
|
9567
|
+
if (!existsSync4(b)) return false;
|
|
9568
|
+
const aBytes = readFileSync4(a);
|
|
9569
|
+
const bBytes = readFileSync4(b);
|
|
8856
9570
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
8857
9571
|
}
|
|
8858
9572
|
async function installBundledPlugin(options = {}) {
|
|
@@ -8865,7 +9579,7 @@ async function installBundledPlugin(options = {}) {
|
|
|
8865
9579
|
}
|
|
8866
9580
|
assertBundledPluginVersion(source);
|
|
8867
9581
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
8868
|
-
const dest =
|
|
9582
|
+
const dest = join4(pluginsFolder, ASSET_NAME);
|
|
8869
9583
|
if (filesMatch(source, dest)) return;
|
|
8870
9584
|
copyFileSync(source, dest);
|
|
8871
9585
|
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
@@ -8879,7 +9593,7 @@ async function installPlugin(options = {}) {
|
|
|
8879
9593
|
const bundled = bundledAssetPath();
|
|
8880
9594
|
if (bundled) {
|
|
8881
9595
|
assertBundledPluginVersion(bundled);
|
|
8882
|
-
const dest2 =
|
|
9596
|
+
const dest2 = join4(pluginsFolder, ASSET_NAME);
|
|
8883
9597
|
if (filesMatch(bundled, dest2)) {
|
|
8884
9598
|
log(`${ASSET_NAME} already installed.`);
|
|
8885
9599
|
return;
|
|
@@ -8894,7 +9608,7 @@ async function installPlugin(options = {}) {
|
|
|
8894
9608
|
if (!asset) {
|
|
8895
9609
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
8896
9610
|
}
|
|
8897
|
-
const dest =
|
|
9611
|
+
const dest = join4(pluginsFolder, ASSET_NAME);
|
|
8898
9612
|
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
8899
9613
|
await download(asset.browser_download_url, dest);
|
|
8900
9614
|
log(`Installed to ${dest}`);
|
|
@@ -8948,7 +9662,8 @@ if (process.argv.includes("--install-plugin")) {
|
|
|
8948
9662
|
const server = new RobloxStudioMCPServer({
|
|
8949
9663
|
name: "robloxstudio-mcp",
|
|
8950
9664
|
version: VERSION,
|
|
8951
|
-
tools: getAllTools()
|
|
9665
|
+
tools: getAllTools(),
|
|
9666
|
+
callableTools: getAllCallableTools()
|
|
8952
9667
|
});
|
|
8953
9668
|
server.run().catch((error) => {
|
|
8954
9669
|
console.error("Server failed to start:", error);
|