@chrrxs/robloxstudio-mcp 2.19.0 → 2.19.1
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 +958 -294
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +226 -120
- package/studio-plugin/MCPPlugin.rbxmx +226 -120
- package/studio-plugin/src/modules/ServerUrlSettings.ts +6 -3
- package/studio-plugin/src/modules/Utils.ts +17 -0
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +68 -3
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +47 -50
- package/studio-plugin/src/modules/handlers/TestHandlers.ts +6 -13
- package/studio-plugin/src/server/index.server.ts +2 -0
package/dist/index.js
CHANGED
|
@@ -451,13 +451,81 @@ var init_bridge_service = __esm({
|
|
|
451
451
|
}
|
|
452
452
|
});
|
|
453
453
|
|
|
454
|
+
// ../core/dist/mcp-compat.js
|
|
455
|
+
import { ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
456
|
+
function registerEmptyResourceShim(server) {
|
|
457
|
+
server.registerCapabilities({ resources: {} });
|
|
458
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
459
|
+
resources: []
|
|
460
|
+
}));
|
|
461
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
462
|
+
resourceTemplates: []
|
|
463
|
+
}));
|
|
464
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
465
|
+
throw new McpError(ErrorCode.InvalidParams, `Resource ${request.params.uri} not found`);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
var init_mcp_compat = __esm({
|
|
469
|
+
"../core/dist/mcp-compat.js"() {
|
|
470
|
+
"use strict";
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
454
474
|
// ../core/dist/http-server.js
|
|
455
475
|
import express from "express";
|
|
456
476
|
import cors from "cors";
|
|
457
477
|
import http from "http";
|
|
458
478
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
459
479
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
460
|
-
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
480
|
+
import { CallToolRequestSchema, ErrorCode as ErrorCode2, ListToolsRequestSchema, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js";
|
|
481
|
+
function parseLineRange(lineRange) {
|
|
482
|
+
const validLine = (line) => line === void 0 || line >= 1;
|
|
483
|
+
if (typeof lineRange === "string") {
|
|
484
|
+
const ranged = lineRange.match(/^\s*(\d+)?\s*[-:]\s*(\d+)?\s*$/);
|
|
485
|
+
if (ranged) {
|
|
486
|
+
const s = ranged[1] !== void 0 ? parseInt(ranged[1], 10) : void 0;
|
|
487
|
+
const e = ranged[2] !== void 0 ? parseInt(ranged[2], 10) : void 0;
|
|
488
|
+
if (!validLine(s) || !validLine(e))
|
|
489
|
+
return void 0;
|
|
490
|
+
if (s !== void 0 && e !== void 0 && s > e)
|
|
491
|
+
return void 0;
|
|
492
|
+
if (s !== void 0 || e !== void 0)
|
|
493
|
+
return { startLine: s, endLine: e };
|
|
494
|
+
}
|
|
495
|
+
const single = lineRange.match(/^\s*(\d+)\s*$/);
|
|
496
|
+
if (single) {
|
|
497
|
+
const n = parseInt(single[1], 10);
|
|
498
|
+
if (n < 1)
|
|
499
|
+
return void 0;
|
|
500
|
+
return { startLine: n, endLine: n };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return void 0;
|
|
504
|
+
}
|
|
505
|
+
function optionalLineRange(body, toolName) {
|
|
506
|
+
if (body.line_range === void 0)
|
|
507
|
+
return {};
|
|
508
|
+
const parsed = parseLineRange(body.line_range);
|
|
509
|
+
if (!parsed)
|
|
510
|
+
throw new Error(`${toolName} line_range must be a string like "42", "10-20", "10-", or "-20"`);
|
|
511
|
+
return parsed;
|
|
512
|
+
}
|
|
513
|
+
function optionalLineAnchor(body, toolName) {
|
|
514
|
+
const parsed = optionalLineRange(body, toolName);
|
|
515
|
+
if (parsed.startLine === void 0 && parsed.endLine === void 0)
|
|
516
|
+
return void 0;
|
|
517
|
+
if (parsed.startLine === void 0 || parsed.endLine === void 0 || parsed.endLine !== parsed.startLine) {
|
|
518
|
+
throw new Error(`${toolName} line_range must be a single line like "42"`);
|
|
519
|
+
}
|
|
520
|
+
return parsed.startLine;
|
|
521
|
+
}
|
|
522
|
+
function requiredClosedLineRange(body, toolName) {
|
|
523
|
+
const parsed = optionalLineRange(body, toolName);
|
|
524
|
+
if (parsed.startLine === void 0 || parsed.endLine === void 0) {
|
|
525
|
+
throw new Error(`${toolName} requires line_range as "start-end" or a single line like "42"`);
|
|
526
|
+
}
|
|
527
|
+
return { startLine: parsed.startLine, endLine: parsed.endLine };
|
|
528
|
+
}
|
|
461
529
|
function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
462
530
|
const app = express();
|
|
463
531
|
let mcpServerActive = false;
|
|
@@ -707,6 +775,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
707
775
|
try {
|
|
708
776
|
trackMCPActivity();
|
|
709
777
|
const server = new Server({ name: serverConfig.name, version: serverConfig.version }, { capabilities: { tools: {} } });
|
|
778
|
+
registerEmptyResourceShim(server);
|
|
710
779
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
711
780
|
tools: filteredTools.map((t) => ({
|
|
712
781
|
name: t.name,
|
|
@@ -717,11 +786,11 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
717
786
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
718
787
|
const { name, arguments: args } = request.params;
|
|
719
788
|
if (allowedTools && !allowedTools.has(name)) {
|
|
720
|
-
throw new
|
|
789
|
+
throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
|
|
721
790
|
}
|
|
722
791
|
const handler = TOOL_HANDLERS[name];
|
|
723
792
|
if (!handler) {
|
|
724
|
-
throw new
|
|
793
|
+
throw new McpError2(ErrorCode2.MethodNotFound, `Unknown tool: ${name}`);
|
|
725
794
|
}
|
|
726
795
|
try {
|
|
727
796
|
return await handler(tools, args || {});
|
|
@@ -739,9 +808,9 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
739
808
|
isError: true
|
|
740
809
|
};
|
|
741
810
|
}
|
|
742
|
-
if (error instanceof
|
|
811
|
+
if (error instanceof McpError2)
|
|
743
812
|
throw error;
|
|
744
|
-
throw new
|
|
813
|
+
throw new McpError2(ErrorCode2.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
745
814
|
}
|
|
746
815
|
});
|
|
747
816
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -847,6 +916,7 @@ var init_http_server = __esm({
|
|
|
847
916
|
"../core/dist/http-server.js"() {
|
|
848
917
|
"use strict";
|
|
849
918
|
init_bridge_service();
|
|
919
|
+
init_mcp_compat();
|
|
850
920
|
TOOL_HANDLERS = {
|
|
851
921
|
get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
|
|
852
922
|
search_files: (tools, body) => tools.searchFiles(body.query, body.searchType, body.instance_id),
|
|
@@ -877,11 +947,17 @@ var init_http_server = __esm({
|
|
|
877
947
|
path: body.path,
|
|
878
948
|
classFilter: body.classFilter
|
|
879
949
|
}, body.instance_id),
|
|
880
|
-
get_script_source: (tools, body) =>
|
|
950
|
+
get_script_source: (tools, body) => {
|
|
951
|
+
const { startLine, endLine } = optionalLineRange(body, "get_script_source");
|
|
952
|
+
return tools.getScriptSource(body.instancePath, startLine, endLine, body.instance_id);
|
|
953
|
+
},
|
|
881
954
|
set_script_source: (tools, body) => tools.setScriptSource(body.instancePath, body.source, body.instance_id),
|
|
882
|
-
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string, body
|
|
955
|
+
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string, optionalLineAnchor(body, "edit_script_lines"), body.instance_id),
|
|
883
956
|
insert_script_lines: (tools, body) => tools.insertScriptLines(body.instancePath, body.afterLine, body.newContent, body.instance_id),
|
|
884
|
-
delete_script_lines: (tools, body) =>
|
|
957
|
+
delete_script_lines: (tools, body) => {
|
|
958
|
+
const { startLine, endLine } = requiredClosedLineRange(body, "delete_script_lines");
|
|
959
|
+
return tools.deleteScriptLines(body.instancePath, startLine, endLine, body.instance_id);
|
|
960
|
+
},
|
|
885
961
|
set_attribute: (tools, body) => tools.setAttribute(body.instancePath, body.attributeName, body.attributeValue, body.valueType, body.instance_id),
|
|
886
962
|
get_attributes: (tools, body) => tools.getAttributes(body.instancePath, body.instance_id),
|
|
887
963
|
delete_attribute: (tools, body) => tools.deleteAttribute(body.instancePath, body.attributeName, body.instance_id),
|
|
@@ -903,8 +979,8 @@ var init_http_server = __esm({
|
|
|
903
979
|
solo_playtest: (tools, body) => tools.soloPlaytest(body.action, body.mode, body.timeout, body.instance_id),
|
|
904
980
|
start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers, body.instance_id),
|
|
905
981
|
stop_playtest: (tools, body) => tools.stopPlaytest(body.instance_id),
|
|
906
|
-
multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id),
|
|
907
|
-
multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id),
|
|
982
|
+
multiplayer_playtest: (tools, body) => tools.multiplayerPlaytest(body.action, body.numPlayers, body.target, body.testArgs, body.value, body.timeout, body.instance_id, body.force),
|
|
983
|
+
multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id, body.force),
|
|
908
984
|
multiplayer_test_state: (tools, body) => tools.multiplayerTestState(body.instance_id),
|
|
909
985
|
multiplayer_test_add_players: (tools, body) => tools.multiplayerTestAddPlayers(body.numPlayers, body.timeout, body.instance_id),
|
|
910
986
|
multiplayer_test_leave_client: (tools, body) => tools.multiplayerTestLeaveClient(body.target, body.timeout, body.instance_id),
|
|
@@ -1739,11 +1815,300 @@ var init_roblox_cookie_client = __esm({
|
|
|
1739
1815
|
}
|
|
1740
1816
|
});
|
|
1741
1817
|
|
|
1742
|
-
// ../core/dist/
|
|
1743
|
-
import
|
|
1744
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
|
|
1818
|
+
// ../core/dist/managed-instance-registry.js
|
|
1819
|
+
import * as fs from "fs";
|
|
1745
1820
|
import * as os from "os";
|
|
1746
1821
|
import * as path from "path";
|
|
1822
|
+
function defaultManagedInstanceRegistryDir() {
|
|
1823
|
+
if (process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR) {
|
|
1824
|
+
return process.env.ROBLOXSTUDIO_MCP_MANAGED_INSTANCE_REGISTRY_DIR;
|
|
1825
|
+
}
|
|
1826
|
+
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
1827
|
+
return path.join(process.env.LOCALAPPDATA, "robloxstudio-mcp", "managed-instances", "v1");
|
|
1828
|
+
}
|
|
1829
|
+
if (process.platform === "darwin") {
|
|
1830
|
+
return path.join(os.homedir(), "Library", "Application Support", "robloxstudio-mcp", "managed-instances", "v1");
|
|
1831
|
+
}
|
|
1832
|
+
return path.join(os.homedir(), ".local", "state", "robloxstudio-mcp", "managed-instances", "v1");
|
|
1833
|
+
}
|
|
1834
|
+
function sleepSync(ms) {
|
|
1835
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1836
|
+
}
|
|
1837
|
+
function ymd(timestamp) {
|
|
1838
|
+
return new Date(timestamp).toISOString().slice(0, 10);
|
|
1839
|
+
}
|
|
1840
|
+
function eventLogDate(name) {
|
|
1841
|
+
const match = name.match(/^events-(\d{4}-\d{2}-\d{2})\.jsonl$/);
|
|
1842
|
+
return match?.[1];
|
|
1843
|
+
}
|
|
1844
|
+
function isRecord(value) {
|
|
1845
|
+
if (!value || typeof value !== "object")
|
|
1846
|
+
return false;
|
|
1847
|
+
const record = value;
|
|
1848
|
+
return record.version === REGISTRY_VERSION && typeof record.recordId === "string" && typeof record.source === "string" && typeof record.exe === "string" && Array.isArray(record.args) && typeof record.launchedAt === "number" && typeof record.bootId === "string";
|
|
1849
|
+
}
|
|
1850
|
+
var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, ManagedInstanceRegistry;
|
|
1851
|
+
var init_managed_instance_registry = __esm({
|
|
1852
|
+
"../core/dist/managed-instance-registry.js"() {
|
|
1853
|
+
"use strict";
|
|
1854
|
+
REGISTRY_VERSION = 1;
|
|
1855
|
+
LOCK_STALE_MS = 1e4;
|
|
1856
|
+
LOCK_RETRY_MS = 25;
|
|
1857
|
+
LOCK_TIMEOUT_MS = 5e3;
|
|
1858
|
+
EVENT_RETENTION_DAYS = 2;
|
|
1859
|
+
ManagedInstanceRegistry = class {
|
|
1860
|
+
dir;
|
|
1861
|
+
constructor(dir = defaultManagedInstanceRegistryDir()) {
|
|
1862
|
+
this.dir = dir;
|
|
1863
|
+
}
|
|
1864
|
+
upsert(record) {
|
|
1865
|
+
this.withLock(() => this.writeRecordUnlocked(record));
|
|
1866
|
+
}
|
|
1867
|
+
attachInstanceId(recordId, instanceId) {
|
|
1868
|
+
this.withLock(() => {
|
|
1869
|
+
const record = this.readRecordUnlocked(recordId);
|
|
1870
|
+
if (!record)
|
|
1871
|
+
return;
|
|
1872
|
+
record.instanceId = instanceId;
|
|
1873
|
+
record.attachedAt = Date.now();
|
|
1874
|
+
this.writeRecordUnlocked(record);
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
findOpenByInstanceId(instanceId, options) {
|
|
1878
|
+
return this.withLock(() => {
|
|
1879
|
+
this.sweepUnlocked(options);
|
|
1880
|
+
return this.readOpenRecordsUnlocked().find((record) => record.instanceId === instanceId);
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
findAnyByInstanceId(instanceId) {
|
|
1884
|
+
return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
|
|
1885
|
+
}
|
|
1886
|
+
listOpen(options) {
|
|
1887
|
+
return this.withLock(() => {
|
|
1888
|
+
this.sweepUnlocked(options);
|
|
1889
|
+
return this.readOpenRecordsUnlocked();
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
markClosed(recordId, closedAt = Date.now()) {
|
|
1893
|
+
this.withLock(() => {
|
|
1894
|
+
const record = this.readRecordUnlocked(recordId);
|
|
1895
|
+
if (!record)
|
|
1896
|
+
return;
|
|
1897
|
+
record.closedAt = closedAt;
|
|
1898
|
+
this.writeRecordUnlocked(record);
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
delete(recordId) {
|
|
1902
|
+
this.withLock(() => this.deleteRecordUnlocked(recordId));
|
|
1903
|
+
}
|
|
1904
|
+
sweep(options) {
|
|
1905
|
+
this.withLock(() => this.sweepUnlocked(options));
|
|
1906
|
+
}
|
|
1907
|
+
logEvent(event, now = Date.now()) {
|
|
1908
|
+
this.withLock(() => this.appendEventUnlocked(event, now));
|
|
1909
|
+
}
|
|
1910
|
+
withLock(fn) {
|
|
1911
|
+
this.ensureDir();
|
|
1912
|
+
const lockDir = path.join(this.dir, ".lock");
|
|
1913
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
1914
|
+
while (true) {
|
|
1915
|
+
try {
|
|
1916
|
+
fs.mkdirSync(lockDir);
|
|
1917
|
+
break;
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
const code = error.code;
|
|
1920
|
+
if (code !== "EEXIST")
|
|
1921
|
+
throw error;
|
|
1922
|
+
try {
|
|
1923
|
+
const stat = fs.statSync(lockDir);
|
|
1924
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
1925
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
} catch {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
if (Date.now() > deadline) {
|
|
1932
|
+
throw new Error(`Timed out waiting for managed instance registry lock: ${lockDir}`);
|
|
1933
|
+
}
|
|
1934
|
+
sleepSync(LOCK_RETRY_MS);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
try {
|
|
1938
|
+
return fn();
|
|
1939
|
+
} finally {
|
|
1940
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
ensureDir() {
|
|
1944
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
1945
|
+
}
|
|
1946
|
+
recordPath(recordId) {
|
|
1947
|
+
return path.join(this.dir, `${recordId}.json`);
|
|
1948
|
+
}
|
|
1949
|
+
recordFilesUnlocked() {
|
|
1950
|
+
return fs.readdirSync(this.dir).filter((name) => name.endsWith(".json")).map((name) => path.join(this.dir, name));
|
|
1951
|
+
}
|
|
1952
|
+
readRecordUnlocked(recordId) {
|
|
1953
|
+
try {
|
|
1954
|
+
const parsed = JSON.parse(fs.readFileSync(this.recordPath(recordId), "utf8"));
|
|
1955
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
1956
|
+
} catch {
|
|
1957
|
+
return void 0;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
readOpenRecordsUnlocked() {
|
|
1961
|
+
return this.readRecordsUnlocked().filter((record) => record.closedAt === void 0);
|
|
1962
|
+
}
|
|
1963
|
+
readRecordsUnlocked() {
|
|
1964
|
+
const records = [];
|
|
1965
|
+
for (const file of this.recordFilesUnlocked()) {
|
|
1966
|
+
try {
|
|
1967
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1968
|
+
if (!isRecord(parsed))
|
|
1969
|
+
continue;
|
|
1970
|
+
records.push(parsed);
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return records;
|
|
1975
|
+
}
|
|
1976
|
+
writeRecordUnlocked(record) {
|
|
1977
|
+
this.ensureDir();
|
|
1978
|
+
const finalPath = this.recordPath(record.recordId);
|
|
1979
|
+
const tmpPath = path.join(this.dir, `${record.recordId}.${process.pid}.${Date.now()}.tmp`);
|
|
1980
|
+
const fd = fs.openSync(tmpPath, "w");
|
|
1981
|
+
try {
|
|
1982
|
+
fs.writeFileSync(fd, `${JSON.stringify(record, null, 2)}
|
|
1983
|
+
`, "utf8");
|
|
1984
|
+
fs.fsyncSync(fd);
|
|
1985
|
+
} finally {
|
|
1986
|
+
fs.closeSync(fd);
|
|
1987
|
+
}
|
|
1988
|
+
fs.renameSync(tmpPath, finalPath);
|
|
1989
|
+
}
|
|
1990
|
+
deleteRecordUnlocked(recordId) {
|
|
1991
|
+
fs.rmSync(this.recordPath(recordId), { force: true });
|
|
1992
|
+
}
|
|
1993
|
+
appendEventUnlocked(event, now) {
|
|
1994
|
+
const file = path.join(this.dir, `events-${ymd(now)}.jsonl`);
|
|
1995
|
+
fs.appendFileSync(file, `${JSON.stringify({
|
|
1996
|
+
ts: new Date(now).toISOString(),
|
|
1997
|
+
...event
|
|
1998
|
+
})}
|
|
1999
|
+
`, "utf8");
|
|
2000
|
+
}
|
|
2001
|
+
cleanupOldEventLogsUnlocked(now) {
|
|
2002
|
+
const cutoff = ymd(now - EVENT_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
|
|
2003
|
+
for (const name of fs.readdirSync(this.dir)) {
|
|
2004
|
+
const date = eventLogDate(name);
|
|
2005
|
+
if (!date || date >= cutoff)
|
|
2006
|
+
continue;
|
|
2007
|
+
try {
|
|
2008
|
+
fs.rmSync(path.join(this.dir, name), { force: true });
|
|
2009
|
+
} catch {
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
cleanupRecord(options, record) {
|
|
2014
|
+
try {
|
|
2015
|
+
options.cleanupRecord?.(record);
|
|
2016
|
+
} catch {
|
|
2017
|
+
this.appendEventUnlocked({
|
|
2018
|
+
event: "registry_cleanup_failed",
|
|
2019
|
+
recordId: record.recordId,
|
|
2020
|
+
instanceId: record.instanceId,
|
|
2021
|
+
source: record.source,
|
|
2022
|
+
reason: "cleanup_record_error"
|
|
2023
|
+
}, options.now ?? Date.now());
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
sweepUnlocked(options) {
|
|
2027
|
+
const now = options.now ?? Date.now();
|
|
2028
|
+
this.cleanupOldEventLogsUnlocked(now);
|
|
2029
|
+
for (const file of this.recordFilesUnlocked()) {
|
|
2030
|
+
let parsed;
|
|
2031
|
+
try {
|
|
2032
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2033
|
+
} catch {
|
|
2034
|
+
fs.rmSync(file, { force: true });
|
|
2035
|
+
this.appendEventUnlocked({
|
|
2036
|
+
event: "registry_pruned_malformed_record",
|
|
2037
|
+
reason: "parse_error",
|
|
2038
|
+
action: "deleted_record"
|
|
2039
|
+
}, now);
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2043
|
+
fs.rmSync(file, { force: true });
|
|
2044
|
+
this.appendEventUnlocked({
|
|
2045
|
+
event: "registry_pruned_malformed_record",
|
|
2046
|
+
reason: "invalid_shape",
|
|
2047
|
+
action: "deleted_record"
|
|
2048
|
+
}, now);
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
const version = parsed.version;
|
|
2052
|
+
if (typeof version === "number" && version > REGISTRY_VERSION)
|
|
2053
|
+
continue;
|
|
2054
|
+
if (!isRecord(parsed)) {
|
|
2055
|
+
fs.rmSync(file, { force: true });
|
|
2056
|
+
this.appendEventUnlocked({
|
|
2057
|
+
event: "registry_pruned_malformed_record",
|
|
2058
|
+
reason: "invalid_shape",
|
|
2059
|
+
action: "deleted_record"
|
|
2060
|
+
}, now);
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
if (parsed.closedAt !== void 0) {
|
|
2064
|
+
fs.rmSync(file, { force: true });
|
|
2065
|
+
this.appendEventUnlocked({
|
|
2066
|
+
event: "registry_pruned_closed_record",
|
|
2067
|
+
recordId: parsed.recordId,
|
|
2068
|
+
instanceId: parsed.instanceId,
|
|
2069
|
+
source: parsed.source,
|
|
2070
|
+
reason: "closed_at_present",
|
|
2071
|
+
action: "deleted_record"
|
|
2072
|
+
}, now);
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
if (parsed.bootId !== options.currentBootId) {
|
|
2076
|
+
this.cleanupRecord(options, parsed);
|
|
2077
|
+
fs.rmSync(file, { force: true });
|
|
2078
|
+
this.appendEventUnlocked({
|
|
2079
|
+
event: "registry_pruned_previous_boot",
|
|
2080
|
+
recordId: parsed.recordId,
|
|
2081
|
+
instanceId: parsed.instanceId,
|
|
2082
|
+
source: parsed.source,
|
|
2083
|
+
reason: "boot_id_changed",
|
|
2084
|
+
action: "deleted_record_and_cleaned_baseplate"
|
|
2085
|
+
}, now);
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
|
|
2089
|
+
this.cleanupRecord(options, parsed);
|
|
2090
|
+
fs.rmSync(file, { force: true });
|
|
2091
|
+
this.appendEventUnlocked({
|
|
2092
|
+
event: "registry_pruned_stale_process",
|
|
2093
|
+
recordId: parsed.recordId,
|
|
2094
|
+
instanceId: parsed.instanceId,
|
|
2095
|
+
source: parsed.source,
|
|
2096
|
+
reason: "pid_not_running",
|
|
2097
|
+
action: "deleted_record_and_cleaned_baseplate"
|
|
2098
|
+
}, now);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
// ../core/dist/studio-instance-manager.js
|
|
2107
|
+
import { execFileSync, spawn } from "child_process";
|
|
2108
|
+
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
2109
|
+
import { randomUUID } from "crypto";
|
|
2110
|
+
import * as os2 from "os";
|
|
2111
|
+
import * as path2 from "path";
|
|
1747
2112
|
function run(command, args, options = {}) {
|
|
1748
2113
|
return execFileSync(command, args, {
|
|
1749
2114
|
encoding: "utf8",
|
|
@@ -1755,7 +2120,7 @@ function isWsl() {
|
|
|
1755
2120
|
if (process.platform !== "linux")
|
|
1756
2121
|
return false;
|
|
1757
2122
|
try {
|
|
1758
|
-
return /microsoft|wsl/i.test(
|
|
2123
|
+
return /microsoft|wsl/i.test(readFileSync2("/proc/version", "utf8"));
|
|
1759
2124
|
} catch {
|
|
1760
2125
|
return false;
|
|
1761
2126
|
}
|
|
@@ -1784,7 +2149,7 @@ function toWslPath(windowsPath) {
|
|
|
1784
2149
|
return run("wslpath", ["-u", windowsPath]);
|
|
1785
2150
|
}
|
|
1786
2151
|
function toStudioLaunchArg(arg) {
|
|
1787
|
-
if (!isWsl() || !
|
|
2152
|
+
if (!isWsl() || !path2.isAbsolute(arg) || !existsSync(arg))
|
|
1788
2153
|
return arg;
|
|
1789
2154
|
return run("wslpath", ["-w", arg]);
|
|
1790
2155
|
}
|
|
@@ -1793,21 +2158,21 @@ function resolveEntrypointDir() {
|
|
|
1793
2158
|
if (!entrypoint)
|
|
1794
2159
|
return void 0;
|
|
1795
2160
|
try {
|
|
1796
|
-
return
|
|
2161
|
+
return path2.dirname(realpathSync(entrypoint));
|
|
1797
2162
|
} catch {
|
|
1798
|
-
return
|
|
2163
|
+
return path2.dirname(path2.resolve(entrypoint));
|
|
1799
2164
|
}
|
|
1800
2165
|
}
|
|
1801
2166
|
function resolveBaseplateTemplatePath() {
|
|
1802
2167
|
const entrypointDir = resolveEntrypointDir();
|
|
1803
2168
|
const candidates = [
|
|
1804
2169
|
...entrypointDir ? [
|
|
1805
|
-
|
|
1806
|
-
|
|
2170
|
+
path2.join(entrypointDir, "assets", BASEPLATE_TEMPLATE_NAME),
|
|
2171
|
+
path2.join(entrypointDir, "..", "assets", BASEPLATE_TEMPLATE_NAME)
|
|
1807
2172
|
] : [],
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2173
|
+
path2.join(process.cwd(), "packages", "core", "assets", BASEPLATE_TEMPLATE_NAME),
|
|
2174
|
+
path2.join(process.cwd(), "packages", "robloxstudio-mcp", "dist", "assets", BASEPLATE_TEMPLATE_NAME),
|
|
2175
|
+
path2.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
|
|
1811
2176
|
];
|
|
1812
2177
|
for (const candidate of candidates) {
|
|
1813
2178
|
if (existsSync(candidate))
|
|
@@ -1816,22 +2181,22 @@ function resolveBaseplateTemplatePath() {
|
|
|
1816
2181
|
throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
|
|
1817
2182
|
}
|
|
1818
2183
|
function createBaseplatePlaceFile() {
|
|
1819
|
-
|
|
1820
|
-
const file =
|
|
2184
|
+
mkdirSync2(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
2185
|
+
const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
1821
2186
|
copyFileSync(resolveBaseplateTemplatePath(), file);
|
|
1822
2187
|
return file;
|
|
1823
2188
|
}
|
|
1824
2189
|
function isGeneratedBaseplatePlaceFile(file) {
|
|
1825
|
-
const resolvedFile =
|
|
1826
|
-
return
|
|
2190
|
+
const resolvedFile = path2.resolve(file);
|
|
2191
|
+
return path2.dirname(resolvedFile) === path2.resolve(BASEPLATE_TEMP_DIR) && BASEPLATE_TEMP_NAME.test(path2.basename(resolvedFile));
|
|
1827
2192
|
}
|
|
1828
2193
|
function cleanupManagedBaseplateFiles(record) {
|
|
1829
2194
|
if (record.source !== "baseplate" || !record.localPlaceFile)
|
|
1830
2195
|
return;
|
|
1831
2196
|
if (!isGeneratedBaseplatePlaceFile(record.localPlaceFile))
|
|
1832
2197
|
return;
|
|
1833
|
-
|
|
1834
|
-
|
|
2198
|
+
rmSync2(record.localPlaceFile, { force: true });
|
|
2199
|
+
rmSync2(`${record.localPlaceFile}.lock`, { force: true });
|
|
1835
2200
|
}
|
|
1836
2201
|
function prepareStudioLaunchOptions(options) {
|
|
1837
2202
|
if (options.source !== "baseplate" || options.localPlaceFile)
|
|
@@ -1851,11 +2216,11 @@ function resolveStudioExe() {
|
|
|
1851
2216
|
throw new Error("Roblox Studio executable auto-discovery is only supported on Windows, WSL, and macOS. Set ROBLOX_STUDIO_EXE.");
|
|
1852
2217
|
}
|
|
1853
2218
|
const localAppData = windowsLocalAppData();
|
|
1854
|
-
const root = localAppData ?
|
|
2219
|
+
const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
1855
2220
|
if (!existsSync(root)) {
|
|
1856
2221
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
1857
2222
|
}
|
|
1858
|
-
const candidates =
|
|
2223
|
+
const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
|
|
1859
2224
|
if (candidates.length === 0) {
|
|
1860
2225
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
1861
2226
|
}
|
|
@@ -1871,14 +2236,14 @@ function listStudioProcesses() {
|
|
|
1871
2236
|
}
|
|
1872
2237
|
return out2.split("\n").filter(Boolean).map((line) => {
|
|
1873
2238
|
const [pid, ...rest] = line.trim().split(/\s+/);
|
|
1874
|
-
return { Id: Number(pid), Path: rest.join(" "), MainWindowTitle: "" };
|
|
2239
|
+
return { Id: Number(pid), Name: "RobloxStudio", Path: rest.join(" "), MainWindowTitle: "" };
|
|
1875
2240
|
}).filter((proc) => Number.isFinite(proc.Id));
|
|
1876
2241
|
}
|
|
1877
2242
|
if (process.platform !== "win32" && !isWsl())
|
|
1878
2243
|
return [];
|
|
1879
2244
|
let out = "";
|
|
1880
2245
|
try {
|
|
1881
|
-
out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | Select-Object Id,Path,MainWindowTitle | ConvertTo-Json -Compress");
|
|
2246
|
+
out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | Select-Object Id,Name,Path,MainWindowTitle | ConvertTo-Json -Compress");
|
|
1882
2247
|
} catch {
|
|
1883
2248
|
return [];
|
|
1884
2249
|
}
|
|
@@ -1887,6 +2252,27 @@ function listStudioProcesses() {
|
|
|
1887
2252
|
const parsed = JSON.parse(out);
|
|
1888
2253
|
return Array.isArray(parsed) ? parsed : [parsed];
|
|
1889
2254
|
}
|
|
2255
|
+
function currentBootId() {
|
|
2256
|
+
if (process.platform === "linux") {
|
|
2257
|
+
try {
|
|
2258
|
+
return readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
2259
|
+
} catch {
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (process.platform === "win32" || isWsl()) {
|
|
2263
|
+
try {
|
|
2264
|
+
return powershell('(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString("o")');
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
if (process.platform === "darwin") {
|
|
2269
|
+
try {
|
|
2270
|
+
return run("sysctl", ["-n", "kern.boottime"]);
|
|
2271
|
+
} catch {
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
return `${process.platform}:${os2.hostname()}:unknown-boot`;
|
|
2275
|
+
}
|
|
1890
2276
|
function buildStudioLaunchArgs(options) {
|
|
1891
2277
|
switch (options.source) {
|
|
1892
2278
|
case "baseplate":
|
|
@@ -1899,13 +2285,13 @@ function buildStudioLaunchArgs(options) {
|
|
|
1899
2285
|
if (!options.placeId)
|
|
1900
2286
|
throw new Error('place_id is required when source="published_place".');
|
|
1901
2287
|
if (!options.universeId)
|
|
1902
|
-
throw new Error('
|
|
2288
|
+
throw new Error('Derived universe id is required when source="published_place".');
|
|
1903
2289
|
return ["--task", "EditPlace", "--placeId", String(options.placeId), "--universeId", String(options.universeId)];
|
|
1904
2290
|
case "place_revision":
|
|
1905
2291
|
if (!options.placeId)
|
|
1906
2292
|
throw new Error('place_id is required when source="place_revision".');
|
|
1907
2293
|
if (!options.universeId)
|
|
1908
|
-
throw new Error('
|
|
2294
|
+
throw new Error('Derived universe id is required when source="place_revision".');
|
|
1909
2295
|
if (!options.placeVersion)
|
|
1910
2296
|
throw new Error('place_version is required when launching source="place_revision".');
|
|
1911
2297
|
return [
|
|
@@ -1923,39 +2309,69 @@ function buildStudioLaunchArgs(options) {
|
|
|
1923
2309
|
function delay(ms) {
|
|
1924
2310
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1925
2311
|
}
|
|
2312
|
+
function basenameAny(filePath) {
|
|
2313
|
+
return path2.basename(filePath.replace(/\\/g, "/"));
|
|
2314
|
+
}
|
|
1926
2315
|
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, StudioInstanceManager;
|
|
1927
2316
|
var init_studio_instance_manager = __esm({
|
|
1928
2317
|
"../core/dist/studio-instance-manager.js"() {
|
|
1929
2318
|
"use strict";
|
|
1930
|
-
|
|
2319
|
+
init_managed_instance_registry();
|
|
2320
|
+
BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
1931
2321
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
1932
2322
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
1933
2323
|
StudioInstanceManager = class {
|
|
1934
2324
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
1935
2325
|
pending = /* @__PURE__ */ new Set();
|
|
2326
|
+
registry;
|
|
2327
|
+
processAdapter;
|
|
2328
|
+
constructor(options = {}) {
|
|
2329
|
+
this.registry = options.registry ?? new ManagedInstanceRegistry(options.registryDir);
|
|
2330
|
+
this.processAdapter = options.processAdapter ?? {};
|
|
2331
|
+
}
|
|
1936
2332
|
list() {
|
|
1937
|
-
|
|
2333
|
+
this.sweepRegistry();
|
|
2334
|
+
const records = [...this.managedByInstanceId.values(), ...this.pending];
|
|
2335
|
+
for (const registryRecord of this.registry.listOpen(this.registrySweepOptions())) {
|
|
2336
|
+
const record = this.fromRegistryRecord(registryRecord);
|
|
2337
|
+
if (records.some((existing) => record.recordId && existing.recordId === record.recordId || record.instanceId && existing.instanceId === record.instanceId)) {
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
records.push(record);
|
|
2341
|
+
}
|
|
2342
|
+
return records.filter((instance, index, all) => all.indexOf(instance) === index);
|
|
1938
2343
|
}
|
|
1939
2344
|
get(instanceId) {
|
|
1940
|
-
|
|
2345
|
+
this.sweepRegistry();
|
|
2346
|
+
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
2347
|
+
if (memoryRecord)
|
|
2348
|
+
return memoryRecord;
|
|
2349
|
+
const registryRecord = this.registry.findOpenByInstanceId(instanceId, this.registrySweepOptions());
|
|
2350
|
+
return registryRecord ? this.fromRegistryRecord(registryRecord) : void 0;
|
|
1941
2351
|
}
|
|
1942
2352
|
attachInstanceId(record, instanceId) {
|
|
1943
2353
|
record.instanceId = instanceId;
|
|
1944
2354
|
this.pending.delete(record);
|
|
1945
2355
|
this.managedByInstanceId.set(instanceId, record);
|
|
2356
|
+
if (record.recordId) {
|
|
2357
|
+
this.registry.attachInstanceId(record.recordId, instanceId);
|
|
2358
|
+
}
|
|
1946
2359
|
}
|
|
1947
2360
|
async launch(options) {
|
|
2361
|
+
this.sweepRegistry();
|
|
1948
2362
|
const preparedOptions = prepareStudioLaunchOptions(options);
|
|
1949
|
-
const before = new Set(listStudioProcesses().map((proc2) => proc2.Id));
|
|
1950
|
-
const exe = resolveStudioExe();
|
|
2363
|
+
const before = new Set(this.listStudioProcesses().map((proc2) => proc2.Id));
|
|
2364
|
+
const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
|
|
1951
2365
|
const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
|
|
1952
|
-
const
|
|
2366
|
+
const spawnOptions = {
|
|
1953
2367
|
cwd: isWsl() && existsSync("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
1954
2368
|
detached: true,
|
|
1955
2369
|
stdio: "ignore"
|
|
1956
|
-
}
|
|
2370
|
+
};
|
|
2371
|
+
const proc = this.processAdapter.spawnStudio ? this.processAdapter.spawnStudio(exe, args, spawnOptions) : spawn(exe, args, spawnOptions);
|
|
1957
2372
|
proc.unref();
|
|
1958
2373
|
const record = {
|
|
2374
|
+
recordId: randomUUID(),
|
|
1959
2375
|
source: options.source,
|
|
1960
2376
|
spawnPid: proc.pid,
|
|
1961
2377
|
exe,
|
|
@@ -1964,43 +2380,119 @@ var init_studio_instance_manager = __esm({
|
|
|
1964
2380
|
universeId: preparedOptions.universeId,
|
|
1965
2381
|
placeVersion: preparedOptions.placeVersion,
|
|
1966
2382
|
localPlaceFile: preparedOptions.localPlaceFile,
|
|
1967
|
-
launchedAt: Date.now()
|
|
2383
|
+
launchedAt: Date.now(),
|
|
2384
|
+
ownerPid: process.pid,
|
|
2385
|
+
bootId: this.getCurrentBootId(),
|
|
2386
|
+
deleteLocalPlaceFileOnClose: options.source === "baseplate"
|
|
1968
2387
|
};
|
|
1969
2388
|
this.pending.add(record);
|
|
2389
|
+
this.registry.upsert(this.toRegistryRecord(record));
|
|
1970
2390
|
const deadline = Date.now() + 5e3;
|
|
1971
2391
|
while (Date.now() < deadline && record.nativeProcessId === void 0) {
|
|
1972
|
-
const created = listStudioProcesses().find((candidate) => !before.has(candidate.Id));
|
|
2392
|
+
const created = this.listStudioProcesses().find((candidate) => !before.has(candidate.Id));
|
|
1973
2393
|
if (created) {
|
|
1974
2394
|
record.nativeProcessId = created.Id;
|
|
2395
|
+
this.registry.upsert(this.toRegistryRecord(record));
|
|
1975
2396
|
break;
|
|
1976
2397
|
}
|
|
1977
2398
|
await delay(250);
|
|
1978
2399
|
}
|
|
1979
2400
|
if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
|
|
1980
2401
|
record.nativeProcessId = proc.pid;
|
|
2402
|
+
this.registry.upsert(this.toRegistryRecord(record));
|
|
1981
2403
|
}
|
|
1982
2404
|
return record;
|
|
1983
2405
|
}
|
|
2406
|
+
closeByInstanceId(instanceId) {
|
|
2407
|
+
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
2408
|
+
if (memoryRecord)
|
|
2409
|
+
return this.close(memoryRecord);
|
|
2410
|
+
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
2411
|
+
if (!registryRecord) {
|
|
2412
|
+
this.sweepRegistry();
|
|
2413
|
+
return { status: "not_found", instanceId };
|
|
2414
|
+
}
|
|
2415
|
+
if (registryRecord.closedAt !== void 0) {
|
|
2416
|
+
this.cleanupManagedRecord(registryRecord);
|
|
2417
|
+
this.registry.delete(registryRecord.recordId);
|
|
2418
|
+
this.registry.logEvent({
|
|
2419
|
+
event: "registry_close_already_stopped",
|
|
2420
|
+
recordId: registryRecord.recordId,
|
|
2421
|
+
instanceId: registryRecord.instanceId,
|
|
2422
|
+
source: registryRecord.source,
|
|
2423
|
+
reason: "closed_at_present",
|
|
2424
|
+
action: "deleted_record_and_cleaned_baseplate"
|
|
2425
|
+
});
|
|
2426
|
+
return { status: "already_closed", instanceId };
|
|
2427
|
+
}
|
|
2428
|
+
if (registryRecord.bootId !== this.getCurrentBootId()) {
|
|
2429
|
+
this.cleanupManagedRecord(registryRecord);
|
|
2430
|
+
this.registry.delete(registryRecord.recordId);
|
|
2431
|
+
this.registry.logEvent({
|
|
2432
|
+
event: "registry_pruned_previous_boot",
|
|
2433
|
+
recordId: registryRecord.recordId,
|
|
2434
|
+
instanceId: registryRecord.instanceId,
|
|
2435
|
+
source: registryRecord.source,
|
|
2436
|
+
reason: "boot_id_changed",
|
|
2437
|
+
action: "deleted_record_and_cleaned_baseplate"
|
|
2438
|
+
});
|
|
2439
|
+
return { status: "already_closed", instanceId };
|
|
2440
|
+
}
|
|
2441
|
+
return this.close(this.fromRegistryRecord(registryRecord));
|
|
2442
|
+
}
|
|
1984
2443
|
close(record) {
|
|
1985
2444
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
1986
2445
|
if (!processId) {
|
|
1987
2446
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
1988
2447
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2448
|
+
const studioProcess = this.findProcessById(processId);
|
|
2449
|
+
if (!studioProcess) {
|
|
2450
|
+
this.cleanupManagedRecord(record);
|
|
2451
|
+
this.markClosedInMemory(record);
|
|
2452
|
+
this.markClosedInRegistry(record);
|
|
2453
|
+
this.registry.logEvent({
|
|
2454
|
+
event: "registry_close_already_stopped",
|
|
2455
|
+
recordId: record.recordId,
|
|
2456
|
+
instanceId: record.instanceId,
|
|
2457
|
+
source: record.source,
|
|
2458
|
+
reason: "pid_not_running",
|
|
2459
|
+
action: "marked_closed_and_cleaned_baseplate"
|
|
2460
|
+
});
|
|
2461
|
+
return { status: "already_closed", instanceId: record.instanceId };
|
|
2462
|
+
}
|
|
2463
|
+
if (!this.verifyProcessForRecord(record, studioProcess)) {
|
|
2464
|
+
this.registry.logEvent({
|
|
2465
|
+
event: "registry_process_verification_failed",
|
|
2466
|
+
recordId: record.recordId,
|
|
2467
|
+
instanceId: record.instanceId,
|
|
2468
|
+
source: record.source,
|
|
2469
|
+
reason: "identity_mismatch"
|
|
2470
|
+
});
|
|
2471
|
+
throw new Error("Managed Studio process identity could not be verified.");
|
|
2472
|
+
}
|
|
2473
|
+
try {
|
|
2474
|
+
this.closeProcess(processId);
|
|
2475
|
+
} catch (error) {
|
|
2476
|
+
if (this.findProcessById(processId))
|
|
2477
|
+
throw error;
|
|
2478
|
+
this.registry.logEvent({
|
|
2479
|
+
event: "registry_close_already_stopped",
|
|
2480
|
+
recordId: record.recordId,
|
|
2481
|
+
instanceId: record.instanceId,
|
|
2482
|
+
source: record.source,
|
|
2483
|
+
reason: "stop_raced_with_exit",
|
|
2484
|
+
action: "marked_closed_and_cleaned_baseplate"
|
|
2485
|
+
});
|
|
2486
|
+
this.cleanupManagedRecord(record);
|
|
2487
|
+
this.markClosedInMemory(record);
|
|
2488
|
+
this.markClosedInRegistry(record);
|
|
2489
|
+
return { status: "already_closed", instanceId: record.instanceId };
|
|
1998
2490
|
}
|
|
1999
2491
|
record.closedAt = Date.now();
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
this.
|
|
2003
|
-
|
|
2492
|
+
this.cleanupManagedRecord(record);
|
|
2493
|
+
this.markClosedInMemory(record);
|
|
2494
|
+
this.markClosedInRegistry(record);
|
|
2495
|
+
return { status: "closed", instanceId: record.instanceId };
|
|
2004
2496
|
}
|
|
2005
2497
|
closeConnectedInstance(instance) {
|
|
2006
2498
|
const process2 = this.findProcessForConnectedInstance(instance);
|
|
@@ -2010,6 +2502,10 @@ var init_studio_instance_manager = __esm({
|
|
|
2010
2502
|
this.closeProcess(process2.Id);
|
|
2011
2503
|
}
|
|
2012
2504
|
closeProcess(processId) {
|
|
2505
|
+
if (this.processAdapter.stopProcess) {
|
|
2506
|
+
this.processAdapter.stopProcess(processId);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2013
2509
|
if (process.platform === "win32" || isWsl()) {
|
|
2014
2510
|
powershell(`Stop-Process -Id ${Math.trunc(processId)} -Force -ErrorAction Stop`);
|
|
2015
2511
|
} else {
|
|
@@ -2022,7 +2518,7 @@ var init_studio_instance_manager = __esm({
|
|
|
2022
2518
|
}
|
|
2023
2519
|
}
|
|
2024
2520
|
findProcessForConnectedInstance(instance) {
|
|
2025
|
-
const processes = listStudioProcesses();
|
|
2521
|
+
const processes = this.listStudioProcesses();
|
|
2026
2522
|
if (processes.length === 0)
|
|
2027
2523
|
return void 0;
|
|
2028
2524
|
if (processes.length === 1)
|
|
@@ -2041,13 +2537,119 @@ var init_studio_instance_manager = __esm({
|
|
|
2041
2537
|
}
|
|
2042
2538
|
return void 0;
|
|
2043
2539
|
}
|
|
2540
|
+
listStudioProcesses() {
|
|
2541
|
+
return this.processAdapter.listStudioProcesses?.() ?? listStudioProcesses();
|
|
2542
|
+
}
|
|
2543
|
+
getCurrentBootId() {
|
|
2544
|
+
return this.processAdapter.currentBootId?.() ?? currentBootId();
|
|
2545
|
+
}
|
|
2546
|
+
registrySweepOptions() {
|
|
2547
|
+
return {
|
|
2548
|
+
currentBootId: this.getCurrentBootId(),
|
|
2549
|
+
isProcessRunning: (record) => this.isRegistryProcessRunning(record),
|
|
2550
|
+
cleanupRecord: (record) => this.cleanupManagedRecord(record)
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
sweepRegistry() {
|
|
2554
|
+
this.registry.sweep(this.registrySweepOptions());
|
|
2555
|
+
}
|
|
2556
|
+
findProcessById(processId) {
|
|
2557
|
+
return this.listStudioProcesses().find((proc) => proc.Id === processId);
|
|
2558
|
+
}
|
|
2559
|
+
isRegistryProcessRunning(record) {
|
|
2560
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
2561
|
+
if (!processId)
|
|
2562
|
+
return true;
|
|
2563
|
+
const studioProcess = this.findProcessById(processId);
|
|
2564
|
+
return !!studioProcess && this.verifyProcessForRecord(this.fromRegistryRecord(record), studioProcess);
|
|
2565
|
+
}
|
|
2566
|
+
verifyProcessForRecord(record, studioProcess) {
|
|
2567
|
+
const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
|
|
2568
|
+
if (!processName.includes("robloxstudio"))
|
|
2569
|
+
return false;
|
|
2570
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
2571
|
+
if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
|
|
2572
|
+
return true;
|
|
2573
|
+
const processPath = studioProcess.Path ? path2.normalize(studioProcess.Path).toLowerCase() : "";
|
|
2574
|
+
const exePath = record.exe ? path2.normalize(record.exe).toLowerCase() : "";
|
|
2575
|
+
if (processPath && exePath && (processPath === exePath || basenameAny(processPath) === basenameAny(exePath))) {
|
|
2576
|
+
return true;
|
|
2577
|
+
}
|
|
2578
|
+
const commandLine = studioProcess.CommandLine ?? "";
|
|
2579
|
+
if (record.localPlaceFile && commandLine.includes(path2.basename(record.localPlaceFile)))
|
|
2580
|
+
return true;
|
|
2581
|
+
if (record.placeId !== void 0 && commandLine.includes(String(record.placeId)))
|
|
2582
|
+
return true;
|
|
2583
|
+
return false;
|
|
2584
|
+
}
|
|
2585
|
+
cleanupManagedRecord(record) {
|
|
2586
|
+
if (record.source !== "baseplate")
|
|
2587
|
+
return;
|
|
2588
|
+
cleanupManagedBaseplateFiles({ source: "baseplate", localPlaceFile: record.localPlaceFile });
|
|
2589
|
+
}
|
|
2590
|
+
markClosedInMemory(record) {
|
|
2591
|
+
record.closedAt = record.closedAt ?? Date.now();
|
|
2592
|
+
if (record.instanceId)
|
|
2593
|
+
this.managedByInstanceId.delete(record.instanceId);
|
|
2594
|
+
this.pending.delete(record);
|
|
2595
|
+
}
|
|
2596
|
+
markClosedInRegistry(record) {
|
|
2597
|
+
if (record.recordId)
|
|
2598
|
+
this.registry.markClosed(record.recordId, record.closedAt ?? Date.now());
|
|
2599
|
+
}
|
|
2600
|
+
toRegistryRecord(record) {
|
|
2601
|
+
if (!record.recordId)
|
|
2602
|
+
throw new Error("Managed Studio record is missing recordId.");
|
|
2603
|
+
if (!record.bootId)
|
|
2604
|
+
throw new Error("Managed Studio record is missing bootId.");
|
|
2605
|
+
return {
|
|
2606
|
+
version: 1,
|
|
2607
|
+
recordId: record.recordId,
|
|
2608
|
+
instanceId: record.instanceId,
|
|
2609
|
+
source: record.source,
|
|
2610
|
+
nativeProcessId: record.nativeProcessId,
|
|
2611
|
+
spawnPid: record.spawnPid,
|
|
2612
|
+
exe: record.exe,
|
|
2613
|
+
args: record.args,
|
|
2614
|
+
placeId: record.placeId,
|
|
2615
|
+
universeId: record.universeId,
|
|
2616
|
+
placeVersion: record.placeVersion,
|
|
2617
|
+
localPlaceFile: record.localPlaceFile,
|
|
2618
|
+
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
|
|
2619
|
+
launchedAt: record.launchedAt,
|
|
2620
|
+
attachedAt: record.instanceId ? Date.now() : void 0,
|
|
2621
|
+
closedAt: record.closedAt,
|
|
2622
|
+
ownerPid: record.ownerPid,
|
|
2623
|
+
bootId: record.bootId
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
fromRegistryRecord(record) {
|
|
2627
|
+
return {
|
|
2628
|
+
recordId: record.recordId,
|
|
2629
|
+
source: record.source,
|
|
2630
|
+
instanceId: record.instanceId,
|
|
2631
|
+
nativeProcessId: record.nativeProcessId,
|
|
2632
|
+
spawnPid: record.spawnPid,
|
|
2633
|
+
exe: record.exe,
|
|
2634
|
+
args: record.args,
|
|
2635
|
+
placeId: record.placeId,
|
|
2636
|
+
universeId: record.universeId,
|
|
2637
|
+
placeVersion: record.placeVersion,
|
|
2638
|
+
localPlaceFile: record.localPlaceFile,
|
|
2639
|
+
launchedAt: record.launchedAt,
|
|
2640
|
+
closedAt: record.closedAt,
|
|
2641
|
+
ownerPid: record.ownerPid,
|
|
2642
|
+
bootId: record.bootId,
|
|
2643
|
+
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2044
2646
|
};
|
|
2045
2647
|
}
|
|
2046
2648
|
});
|
|
2047
2649
|
|
|
2048
2650
|
// ../core/dist/image-decode.js
|
|
2049
|
-
import * as
|
|
2050
|
-
import * as
|
|
2651
|
+
import * as fs2 from "fs";
|
|
2652
|
+
import * as path3 from "path";
|
|
2051
2653
|
import { inflateSync } from "zlib";
|
|
2052
2654
|
function paethPredictor(a, b, c) {
|
|
2053
2655
|
const p = a + b - c;
|
|
@@ -2205,11 +2807,11 @@ function decodePngToRgba(data) {
|
|
|
2205
2807
|
return resizeRgbaNearest({ width, height, rgba });
|
|
2206
2808
|
}
|
|
2207
2809
|
function decodeImagePathToRgba(imagePath) {
|
|
2208
|
-
const resolved =
|
|
2209
|
-
if (!
|
|
2810
|
+
const resolved = path3.resolve(imagePath);
|
|
2811
|
+
if (!fs2.existsSync(resolved)) {
|
|
2210
2812
|
throw new Error(`image_path not found: ${imagePath}`);
|
|
2211
2813
|
}
|
|
2212
|
-
return decodePngToRgba(
|
|
2814
|
+
return decodePngToRgba(fs2.readFileSync(resolved));
|
|
2213
2815
|
}
|
|
2214
2816
|
var PNG_SIGNATURE, MAX_EDITABLE_IMAGE_DIMENSION;
|
|
2215
2817
|
var init_image_decode = __esm({
|
|
@@ -3237,9 +3839,19 @@ var init_png_encoder = __esm({
|
|
|
3237
3839
|
});
|
|
3238
3840
|
|
|
3239
3841
|
// ../core/dist/tools/index.js
|
|
3240
|
-
import * as
|
|
3241
|
-
import * as
|
|
3242
|
-
import * as
|
|
3842
|
+
import * as fs3 from "fs";
|
|
3843
|
+
import * as os3 from "os";
|
|
3844
|
+
import * as path4 from "path";
|
|
3845
|
+
function multiplayerStopDisabledBody() {
|
|
3846
|
+
return {
|
|
3847
|
+
success: false,
|
|
3848
|
+
error: "multiplayer_stop_disabled",
|
|
3849
|
+
message: MULTIPLAYER_STOP_DISABLED_MESSAGE,
|
|
3850
|
+
reason: "StudioTestService:EndTest does not reliably end StudioTestService multiplayer sessions from MCP right now.",
|
|
3851
|
+
manualCleanupRequired: true,
|
|
3852
|
+
recoveryHint: "Close the Roblox Studio multiplayer test windows manually."
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3243
3855
|
function encodeImageFromRgbaResponse(response, format, quality) {
|
|
3244
3856
|
if (!response.data || response.width === void 0 || response.height === void 0) {
|
|
3245
3857
|
throw new Error("Render response missing data, width, or height");
|
|
@@ -3316,8 +3928,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
|
|
|
3316
3928
|
if (typeof sourcePath !== "string" || sourcePath === "") {
|
|
3317
3929
|
throw new Error("baseline_path must be a non-empty string when provided");
|
|
3318
3930
|
}
|
|
3319
|
-
const resolved =
|
|
3320
|
-
const parsed = JSON.parse(
|
|
3931
|
+
const resolved = path4.resolve(sourcePath);
|
|
3932
|
+
const parsed = JSON.parse(fs3.readFileSync(resolved, "utf8"));
|
|
3321
3933
|
const record = asRecord(parsed);
|
|
3322
3934
|
if (!record)
|
|
3323
3935
|
throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
|
|
@@ -3765,7 +4377,7 @@ end
|
|
|
3765
4377
|
error("Unsupported device simulator operation: " .. tostring(opts.operation), 0)
|
|
3766
4378
|
`.trim();
|
|
3767
4379
|
}
|
|
3768
|
-
var MAX_INLINE_IMAGE_BYTES, MAX_DEVICE_MATRIX_ENTRIES, MAX_NETWORK_PACKET_LOSS_PERCENT, STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL, NETWORK_PROFILE_KEYS, NETWORK_PROFILES, ZERO_NETWORK_PROFILE, SIMULATION_PERSISTENCE_NOTES, RobloxStudioTools;
|
|
4380
|
+
var MAX_INLINE_IMAGE_BYTES, MAX_DEVICE_MATRIX_ENTRIES, MAX_NETWORK_PACKET_LOSS_PERCENT, STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL, MULTIPLAYER_FORCE_REQUIRED_MESSAGE, MULTIPLAYER_STOP_DISABLED_MESSAGE, NETWORK_PROFILE_KEYS, NETWORK_PROFILES, ZERO_NETWORK_PROFILE, SIMULATION_PERSISTENCE_NOTES, RobloxStudioTools;
|
|
3769
4381
|
var init_tools = __esm({
|
|
3770
4382
|
"../core/dist/tools/index.js"() {
|
|
3771
4383
|
"use strict";
|
|
@@ -3782,6 +4394,8 @@ var init_tools = __esm({
|
|
|
3782
4394
|
MAX_DEVICE_MATRIX_ENTRIES = 6;
|
|
3783
4395
|
MAX_NETWORK_PACKET_LOSS_PERCENT = 0.5;
|
|
3784
4396
|
STUDIO_ASSISTANT_SOURCE_IMAGE_LABEL = "Studio Assistant Source Image";
|
|
4397
|
+
MULTIPLAYER_FORCE_REQUIRED_MESSAGE = "StudioTestService multiplayer stop is currently disabled because StudioTestService:EndTest is broken for this flow. Pass force=true only if you understand you must manually close the multiplayer test windows afterward.";
|
|
4398
|
+
MULTIPLAYER_STOP_DISABLED_MESSAGE = "Multiplayer playtest stop/end is disabled because StudioTestService:EndTest is currently broken for this flow. Manually close the Studio multiplayer test windows instead.";
|
|
3785
4399
|
NETWORK_PROFILE_KEYS = [
|
|
3786
4400
|
"InboundNetworkMinDelayMs",
|
|
3787
4401
|
"OutboundNetworkMinDelayMs",
|
|
@@ -4186,8 +4800,8 @@ var init_tools = __esm({
|
|
|
4186
4800
|
timedOut: true
|
|
4187
4801
|
};
|
|
4188
4802
|
}
|
|
4189
|
-
async getFileTree(
|
|
4190
|
-
const response = await this._callSingle("/api/file-tree", { path:
|
|
4803
|
+
async getFileTree(path5 = "", instance_id) {
|
|
4804
|
+
const response = await this._callSingle("/api/file-tree", { path: path5 }, void 0, instance_id);
|
|
4191
4805
|
return {
|
|
4192
4806
|
content: [
|
|
4193
4807
|
{
|
|
@@ -4304,9 +4918,9 @@ var init_tools = __esm({
|
|
|
4304
4918
|
]
|
|
4305
4919
|
};
|
|
4306
4920
|
}
|
|
4307
|
-
async getProjectStructure(
|
|
4921
|
+
async getProjectStructure(path5, maxDepth, scriptsOnly, instance_id) {
|
|
4308
4922
|
const response = await this._callSingle("/api/project-structure", {
|
|
4309
|
-
path:
|
|
4923
|
+
path: path5,
|
|
4310
4924
|
maxDepth,
|
|
4311
4925
|
scriptsOnly
|
|
4312
4926
|
}, void 0, instance_id);
|
|
@@ -4486,17 +5100,20 @@ var init_tools = __esm({
|
|
|
4486
5100
|
const topService = typeof response.topService === "string" && response.topService.length > 0 ? response.topService : pathSegments[0] === "game" ? pathSegments[1] ?? "game" : pathSegments[0];
|
|
4487
5101
|
const typeNote = scriptTypeInfo[response.className] || response.className;
|
|
4488
5102
|
const serviceNote = serviceInfo[topService] || topService;
|
|
5103
|
+
const showRange = Boolean(response.isPartial || response.truncated) && response.startLine !== void 0 && response.endLine !== void 0;
|
|
4489
5104
|
const headerLines = [
|
|
4490
5105
|
`Path: ${pathStr}`,
|
|
4491
5106
|
`Type: ${typeNote}`,
|
|
4492
5107
|
`Location: ${serviceNote}`,
|
|
4493
|
-
`Lines: ${response.lineCount} total${
|
|
5108
|
+
`Lines: ${response.lineCount} total${showRange ? ` (showing ${response.startLine}-${response.endLine})` : ""}`
|
|
4494
5109
|
];
|
|
4495
5110
|
if (response.enabled === false) {
|
|
4496
5111
|
headerLines.push(`Status: DISABLED`);
|
|
4497
5112
|
}
|
|
4498
|
-
if (response.
|
|
4499
|
-
headerLines.push(`Note:
|
|
5113
|
+
if (typeof response.note === "string" && response.note.length > 0) {
|
|
5114
|
+
headerLines.push(`Note: ${response.note}`);
|
|
5115
|
+
} else if (response.truncated) {
|
|
5116
|
+
headerLines.push(`Note: Truncated response; use line_range to read more`);
|
|
4500
5117
|
}
|
|
4501
5118
|
const header = headerLines.join("\n");
|
|
4502
5119
|
const code = response.numberedSource || response.source;
|
|
@@ -4574,7 +5191,7 @@ ${code}`
|
|
|
4574
5191
|
}
|
|
4575
5192
|
const response = await this._callSingle("/api/grep-scripts", {
|
|
4576
5193
|
pattern,
|
|
4577
|
-
...options
|
|
5194
|
+
...options ?? {}
|
|
4578
5195
|
}, void 0, instance_id);
|
|
4579
5196
|
return {
|
|
4580
5197
|
content: [
|
|
@@ -5227,9 +5844,9 @@ ${code}`
|
|
|
5227
5844
|
const rawJson = mutable.raw_json;
|
|
5228
5845
|
if (typeof rawJson === "string") {
|
|
5229
5846
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
5230
|
-
const resolvedOutputPath =
|
|
5231
|
-
|
|
5232
|
-
|
|
5847
|
+
const resolvedOutputPath = path4.resolve(outputPath);
|
|
5848
|
+
fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
|
|
5849
|
+
fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
|
|
5233
5850
|
mutable.output_path = resolvedOutputPath;
|
|
5234
5851
|
}
|
|
5235
5852
|
delete mutable.raw_json;
|
|
@@ -5290,9 +5907,9 @@ ${code}`
|
|
|
5290
5907
|
const rawSnapshotBase64 = mutable.raw_snapshot_base64;
|
|
5291
5908
|
if (typeof rawSnapshotBase64 === "string") {
|
|
5292
5909
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
5293
|
-
const resolvedOutputPath =
|
|
5294
|
-
|
|
5295
|
-
|
|
5910
|
+
const resolvedOutputPath = path4.resolve(outputPath);
|
|
5911
|
+
fs3.mkdirSync(path4.dirname(resolvedOutputPath), { recursive: true });
|
|
5912
|
+
fs3.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
|
|
5296
5913
|
mutable.output_path = resolvedOutputPath;
|
|
5297
5914
|
}
|
|
5298
5915
|
delete mutable.raw_snapshot_base64;
|
|
@@ -5306,9 +5923,9 @@ ${code}`
|
|
|
5306
5923
|
});
|
|
5307
5924
|
}
|
|
5308
5925
|
if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
|
|
5309
|
-
const resolvedSummaryPath =
|
|
5310
|
-
|
|
5311
|
-
|
|
5926
|
+
const resolvedSummaryPath = path4.resolve(summaryOutputPath);
|
|
5927
|
+
fs3.mkdirSync(path4.dirname(resolvedSummaryPath), { recursive: true });
|
|
5928
|
+
fs3.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
|
|
5312
5929
|
mutable.summary_output_path = resolvedSummaryPath;
|
|
5313
5930
|
}
|
|
5314
5931
|
if (!includeComparisonIndex) {
|
|
@@ -5355,6 +5972,14 @@ ${code}`
|
|
|
5355
5972
|
return void 0;
|
|
5356
5973
|
return this._positiveInteger(value, name);
|
|
5357
5974
|
}
|
|
5975
|
+
_optionalFiniteNumber(value, name) {
|
|
5976
|
+
if (value === void 0 || value === null)
|
|
5977
|
+
return void 0;
|
|
5978
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
5979
|
+
throw new Error(`${name} must be a finite number.`);
|
|
5980
|
+
}
|
|
5981
|
+
return value;
|
|
5982
|
+
}
|
|
5358
5983
|
_publicInstanceKey(instance) {
|
|
5359
5984
|
return `${instance.instanceId}:${instance.role}:${instance.connectedAt}`;
|
|
5360
5985
|
}
|
|
@@ -5367,7 +5992,7 @@ ${code}`
|
|
|
5367
5992
|
return record.placeId !== void 0 && instance.placeId === record.placeId;
|
|
5368
5993
|
}
|
|
5369
5994
|
if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
|
|
5370
|
-
const expectedName =
|
|
5995
|
+
const expectedName = path4.basename(record.localPlaceFile);
|
|
5371
5996
|
return instance.placeName === expectedName || instance.dataModelName === expectedName;
|
|
5372
5997
|
}
|
|
5373
5998
|
return true;
|
|
@@ -5376,11 +6001,11 @@ ${code}`
|
|
|
5376
6001
|
const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
|
|
5377
6002
|
if (!response.ok) {
|
|
5378
6003
|
const body = await response.text().catch(() => "");
|
|
5379
|
-
throw new Error(`Could not
|
|
6004
|
+
throw new Error(`Could not resolve the universe for place_id ${placeId} (${response.status}): ${body}`);
|
|
5380
6005
|
}
|
|
5381
6006
|
const data = await response.json();
|
|
5382
6007
|
if (typeof data.universeId !== "number" || !Number.isFinite(data.universeId)) {
|
|
5383
|
-
throw new Error(`Could not
|
|
6008
|
+
throw new Error(`Could not resolve the universe for place_id ${placeId}.`);
|
|
5384
6009
|
}
|
|
5385
6010
|
return Math.trunc(data.universeId);
|
|
5386
6011
|
}
|
|
@@ -5422,7 +6047,7 @@ ${code}`
|
|
|
5422
6047
|
});
|
|
5423
6048
|
}
|
|
5424
6049
|
const placeId2 = this._positiveInteger(request.place_id, "place_id");
|
|
5425
|
-
const rawMaxPageSize = this.
|
|
6050
|
+
const rawMaxPageSize = Math.trunc(this._optionalFiniteNumber(request.max_page_size, "max_page_size") ?? 10);
|
|
5426
6051
|
const maxPageSize = Math.max(1, Math.min(50, rawMaxPageSize));
|
|
5427
6052
|
const pageToken = typeof request.page_token === "string" ? request.page_token : void 0;
|
|
5428
6053
|
const response = await this.openCloudClient.listAssetVersions(placeId2, maxPageSize, pageToken);
|
|
@@ -5467,31 +6092,38 @@ ${code}`
|
|
|
5467
6092
|
if (action === "close") {
|
|
5468
6093
|
let record2;
|
|
5469
6094
|
if (instance_id) {
|
|
5470
|
-
|
|
5471
|
-
if (
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
if (!edit) {
|
|
5475
|
-
return this._textResult({
|
|
5476
|
-
error: "Instance is not connected or managed.",
|
|
5477
|
-
instance_id
|
|
5478
|
-
});
|
|
5479
|
-
}
|
|
5480
|
-
try {
|
|
5481
|
-
this.instanceManager.closeConnectedInstance(edit);
|
|
5482
|
-
await sleep(500);
|
|
5483
|
-
} catch (error) {
|
|
5484
|
-
return this._textResult({
|
|
5485
|
-
error: error instanceof Error ? error.message : String(error),
|
|
5486
|
-
instance_id
|
|
5487
|
-
});
|
|
5488
|
-
}
|
|
6095
|
+
const managedClose = this.instanceManager.closeByInstanceId(instance_id);
|
|
6096
|
+
if (managedClose.status !== "not_found") {
|
|
6097
|
+
this.bridge.unregisterInstanceId(instance_id);
|
|
6098
|
+
await sleep(500);
|
|
5489
6099
|
this.bridge.unregisterInstanceId(instance_id);
|
|
5490
6100
|
return this._textResult({
|
|
5491
6101
|
instance_id,
|
|
5492
|
-
message: "Studio instance closed."
|
|
6102
|
+
message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
6103
|
+
});
|
|
6104
|
+
}
|
|
6105
|
+
const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
|
|
6106
|
+
const edit = connected2.find((instance) => instance.role === "edit");
|
|
6107
|
+
if (!edit) {
|
|
6108
|
+
return this._textResult({
|
|
6109
|
+
error: "Instance is not connected or managed.",
|
|
6110
|
+
instance_id
|
|
6111
|
+
});
|
|
6112
|
+
}
|
|
6113
|
+
try {
|
|
6114
|
+
this.instanceManager.closeConnectedInstance(edit);
|
|
6115
|
+
await sleep(500);
|
|
6116
|
+
} catch (error) {
|
|
6117
|
+
return this._textResult({
|
|
6118
|
+
error: error instanceof Error ? error.message : String(error),
|
|
6119
|
+
instance_id
|
|
5493
6120
|
});
|
|
5494
6121
|
}
|
|
6122
|
+
this.bridge.unregisterInstanceId(instance_id);
|
|
6123
|
+
return this._textResult({
|
|
6124
|
+
instance_id,
|
|
6125
|
+
message: "Studio instance closed."
|
|
6126
|
+
});
|
|
5495
6127
|
} else {
|
|
5496
6128
|
const active = this.instanceManager.list().filter((entry) => entry.closedAt === void 0);
|
|
5497
6129
|
if (active.length === 0) {
|
|
@@ -5507,14 +6139,14 @@ ${code}`
|
|
|
5507
6139
|
}
|
|
5508
6140
|
if (record2.instanceId)
|
|
5509
6141
|
this.bridge.unregisterInstanceId(record2.instanceId);
|
|
5510
|
-
this.instanceManager.close(record2);
|
|
6142
|
+
const closeResult = this.instanceManager.close(record2);
|
|
5511
6143
|
if (record2.instanceId) {
|
|
5512
6144
|
await sleep(500);
|
|
5513
6145
|
this.bridge.unregisterInstanceId(record2.instanceId);
|
|
5514
6146
|
}
|
|
5515
6147
|
return this._textResult({
|
|
5516
6148
|
instance_id: record2.instanceId,
|
|
5517
|
-
message: "Studio instance closed."
|
|
6149
|
+
message: closeResult.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
5518
6150
|
});
|
|
5519
6151
|
}
|
|
5520
6152
|
const source = request.source;
|
|
@@ -5522,8 +6154,8 @@ ${code}`
|
|
|
5522
6154
|
throw new Error("manage_instance action=launch requires source=baseplate|local_file|published_place|place_revision");
|
|
5523
6155
|
}
|
|
5524
6156
|
const launchSource = source;
|
|
5525
|
-
const placeId = launchSource === "published_place" || launchSource === "place_revision" ? this._positiveInteger(request.place_id, "place_id") :
|
|
5526
|
-
const placeVersion = launchSource === "place_revision" ? this._positiveInteger(request.place_version, "place_version") :
|
|
6157
|
+
const placeId = launchSource === "published_place" || launchSource === "place_revision" ? this._positiveInteger(request.place_id, "place_id") : void 0;
|
|
6158
|
+
const placeVersion = launchSource === "place_revision" ? this._positiveInteger(request.place_version, "place_version") : void 0;
|
|
5527
6159
|
const localPlaceFile = typeof request.local_place_file === "string" ? request.local_place_file : void 0;
|
|
5528
6160
|
if (launchSource === "published_place" && placeId !== void 0 && this._isLatestPublishedPlaceOpen(placeId)) {
|
|
5529
6161
|
return this._textResult({
|
|
@@ -5531,7 +6163,7 @@ ${code}`
|
|
|
5531
6163
|
message: `place_id ${placeId} is already connected. Use the existing instance or launch a specific place_revision.`
|
|
5532
6164
|
});
|
|
5533
6165
|
}
|
|
5534
|
-
const universeId = launchSource === "published_place" || launchSource === "place_revision" ?
|
|
6166
|
+
const universeId = launchSource === "published_place" || launchSource === "place_revision" ? await this._deriveUniverseId(placeId) : void 0;
|
|
5535
6167
|
const waitForConnection = request.wait_for_connection !== false;
|
|
5536
6168
|
const timeoutMs = this._optionalPositiveInteger(request.timeout_ms, "timeout_ms") ?? 12e4;
|
|
5537
6169
|
const beforeKeys = new Set(this.bridge.getPublicInstances().map((instance) => this._publicInstanceKey(instance)));
|
|
@@ -5797,26 +6429,40 @@ ${code}`
|
|
|
5797
6429
|
}
|
|
5798
6430
|
return hasServer && clientCount >= 2;
|
|
5799
6431
|
}
|
|
5800
|
-
async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30) {
|
|
6432
|
+
async _waitForMultiplayerStart(instanceId, clientCount, timeoutSec = 30, connectedAfter) {
|
|
5801
6433
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
6434
|
+
let lastPhase;
|
|
5802
6435
|
while (Date.now() < deadline) {
|
|
5803
6436
|
const exact = await this._waitForExactClientCount(instanceId, clientCount, 0.25, 0);
|
|
5804
6437
|
if (exact.ok || exact.extraClients) {
|
|
6438
|
+
if (exact.ok && connectedAfter !== void 0) {
|
|
6439
|
+
const instances = this.bridge.getInstances().filter((inst) => inst.instanceId === instanceId);
|
|
6440
|
+
const freshRoles = new Set(instances.filter((inst) => inst.connectedAt >= connectedAfter).map((inst) => inst.role));
|
|
6441
|
+
const freshClientCount = [...freshRoles].filter((role) => /^client-\d+$/.test(role)).length;
|
|
6442
|
+
if (!freshRoles.has("server") || freshClientCount !== clientCount) {
|
|
6443
|
+
await sleep(250);
|
|
6444
|
+
continue;
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
5805
6447
|
return { ok: exact.ok, roles: exact.roles, timedOut: false, error: exact.extraClients ? `Expected ${clientCount} client(s), but Studio registered ${exact.clientCount}.` : void 0 };
|
|
5806
6448
|
}
|
|
5807
6449
|
try {
|
|
5808
|
-
const
|
|
6450
|
+
const remainingMs = Math.max(1, Math.min(1e3, deadline - Date.now()));
|
|
6451
|
+
const editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit", remainingMs);
|
|
5809
6452
|
const session = editState?.session;
|
|
5810
|
-
if (
|
|
6453
|
+
if (typeof session?.phase === "string") {
|
|
6454
|
+
lastPhase = session.phase;
|
|
6455
|
+
}
|
|
6456
|
+
if (session?.phase === "failed") {
|
|
5811
6457
|
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: false, phase: session.phase, error: session.error };
|
|
5812
6458
|
}
|
|
5813
6459
|
} catch {
|
|
5814
6460
|
}
|
|
5815
6461
|
await sleep(250);
|
|
5816
6462
|
}
|
|
5817
|
-
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true };
|
|
6463
|
+
return { ok: false, roles: this._rolesForInstance(instanceId), timedOut: true, phase: lastPhase };
|
|
5818
6464
|
}
|
|
5819
|
-
async multiplayerPlaytest(action, numPlayers, target, testArgs, value, timeout, instance_id) {
|
|
6465
|
+
async multiplayerPlaytest(action, numPlayers, target, testArgs, value, timeout, instance_id, force) {
|
|
5820
6466
|
if (action !== "start" && action !== "status" && action !== "add_players" && action !== "leave_client" && action !== "end") {
|
|
5821
6467
|
throw new Error("multiplayer_playtest requires action=start|status|add_players|leave_client|end");
|
|
5822
6468
|
}
|
|
@@ -5838,93 +6484,130 @@ ${code}`
|
|
|
5838
6484
|
});
|
|
5839
6485
|
}
|
|
5840
6486
|
if (action === "start") {
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
6487
|
+
if (force !== true) {
|
|
6488
|
+
return this._textResult({
|
|
6489
|
+
success: false,
|
|
6490
|
+
action,
|
|
6491
|
+
error: "multiplayer_force_required",
|
|
6492
|
+
message: MULTIPLAYER_FORCE_REQUIRED_MESSAGE,
|
|
6493
|
+
requiresForce: true,
|
|
6494
|
+
manualCleanupRequired: true
|
|
6495
|
+
});
|
|
6496
|
+
}
|
|
6497
|
+
const body = this._parseTextResult(await this.multiplayerTestStart(numPlayers, testArgs, timeout, instance_id, force));
|
|
6498
|
+
const state = body.state && typeof body.state === "object" ? body.state : {};
|
|
6499
|
+
const launched = body.success === true && body.ready === true;
|
|
6500
|
+
return this._textResult(launched ? {
|
|
5845
6501
|
success: true,
|
|
5846
6502
|
action,
|
|
5847
|
-
message: "Multiplayer playtest started.",
|
|
5848
|
-
|
|
6503
|
+
message: "Multiplayer playtest started. Stop/end is disabled; close the multiplayer test windows manually when finished.",
|
|
6504
|
+
ready: true,
|
|
6505
|
+
manualCleanupRequired: true,
|
|
6506
|
+
roles: Array.isArray(body.roles) ? body.roles : void 0,
|
|
5849
6507
|
clientRoles: Array.isArray(state.clientRoles) ? state.clientRoles : void 0,
|
|
5850
6508
|
playerCount: typeof state.playerCount === "number" ? state.playerCount : void 0
|
|
5851
6509
|
} : {
|
|
5852
6510
|
success: false,
|
|
5853
6511
|
action,
|
|
5854
|
-
error:
|
|
5855
|
-
message:
|
|
5856
|
-
|
|
6512
|
+
error: body.error ?? body.wait?.error ?? "multiplayer_start_not_detected",
|
|
6513
|
+
message: body.success === true ? "Multiplayer playtest start was requested, but MCP did not detect the required server/client peers before timeout. You may need to close the test windows manually." : body.message ?? "Multiplayer playtest did not start.",
|
|
6514
|
+
manualCleanupRequired: body.startRequested === true || body.launched === true ? true : void 0,
|
|
6515
|
+
roles: Array.isArray(body.roles) ? body.roles : void 0
|
|
5857
6516
|
});
|
|
5858
6517
|
}
|
|
5859
6518
|
if (action === "add_players") {
|
|
5860
|
-
const
|
|
5861
|
-
const state =
|
|
5862
|
-
const success =
|
|
6519
|
+
const body = this._parseTextResult(await this.multiplayerTestAddPlayers(numPlayers, timeout, instance_id));
|
|
6520
|
+
const state = body.state && typeof body.state === "object" ? body.state : {};
|
|
6521
|
+
const success = body.success === true && body.ready === true;
|
|
5863
6522
|
return this._textResult(success ? {
|
|
5864
6523
|
success: true,
|
|
5865
6524
|
action,
|
|
5866
6525
|
message: "Players added.",
|
|
5867
|
-
roles: Array.isArray(
|
|
6526
|
+
roles: Array.isArray(body.roles) ? body.roles : void 0,
|
|
5868
6527
|
clientRoles: Array.isArray(state.clientRoles) ? state.clientRoles : void 0,
|
|
5869
6528
|
playerCount: typeof state.playerCount === "number" ? state.playerCount : void 0
|
|
5870
6529
|
} : {
|
|
5871
6530
|
success: false,
|
|
5872
6531
|
action,
|
|
5873
|
-
error:
|
|
5874
|
-
message:
|
|
5875
|
-
roles: Array.isArray(
|
|
6532
|
+
error: body.error ?? "add_players_failed",
|
|
6533
|
+
message: body.success === true ? "Players did not finish joining before timeout." : body.message ?? "Players were not added.",
|
|
6534
|
+
roles: Array.isArray(body.roles) ? body.roles : void 0
|
|
5876
6535
|
});
|
|
5877
6536
|
}
|
|
5878
6537
|
if (action === "leave_client") {
|
|
5879
|
-
const
|
|
5880
|
-
return this._textResult(
|
|
6538
|
+
const body = this._parseTextResult(await this.multiplayerTestLeaveClient(target ?? "client-1", timeout, instance_id));
|
|
6539
|
+
return this._textResult(body.success === true && body.left === true ? {
|
|
5881
6540
|
success: true,
|
|
5882
6541
|
action,
|
|
5883
6542
|
message: "Client left.",
|
|
5884
|
-
roles: Array.isArray(
|
|
6543
|
+
roles: Array.isArray(body.roles) ? body.roles : void 0
|
|
5885
6544
|
} : {
|
|
5886
6545
|
success: false,
|
|
5887
6546
|
action,
|
|
5888
|
-
error:
|
|
5889
|
-
message:
|
|
5890
|
-
roles: Array.isArray(
|
|
6547
|
+
error: body.error ?? "leave_client_failed",
|
|
6548
|
+
message: body.message ?? "Client did not leave.",
|
|
6549
|
+
roles: Array.isArray(body.roles) ? body.roles : void 0
|
|
5891
6550
|
});
|
|
5892
6551
|
}
|
|
5893
|
-
|
|
5894
|
-
return this._textResult(body.success === true && body.ended === true ? {
|
|
5895
|
-
success: true,
|
|
5896
|
-
action,
|
|
5897
|
-
message: "Multiplayer playtest ended."
|
|
5898
|
-
} : {
|
|
5899
|
-
success: false,
|
|
6552
|
+
return this._textResult({
|
|
5900
6553
|
action,
|
|
5901
|
-
|
|
5902
|
-
message: body.message ?? "Multiplayer playtest did not end.",
|
|
5903
|
-
roles: Array.isArray(body.roles) ? body.roles : void 0,
|
|
5904
|
-
editDone: body.editDone === false ? false : void 0
|
|
6554
|
+
...multiplayerStopDisabledBody()
|
|
5905
6555
|
});
|
|
5906
6556
|
}
|
|
5907
|
-
async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id) {
|
|
6557
|
+
async multiplayerTestStart(numPlayers, testArgs, timeout, instance_id, force) {
|
|
5908
6558
|
if (!Number.isInteger(numPlayers) || numPlayers < 1 || numPlayers > 8) {
|
|
5909
6559
|
throw new Error("numPlayers must be an integer from 1 to 8");
|
|
5910
6560
|
}
|
|
5911
6561
|
const editTarget = this._resolveSingleTarget("edit", instance_id);
|
|
6562
|
+
if (force !== true) {
|
|
6563
|
+
return this._textResult({
|
|
6564
|
+
success: false,
|
|
6565
|
+
error: "multiplayer_force_required",
|
|
6566
|
+
message: MULTIPLAYER_FORCE_REQUIRED_MESSAGE,
|
|
6567
|
+
requiresForce: true,
|
|
6568
|
+
manualCleanupRequired: true
|
|
6569
|
+
});
|
|
6570
|
+
}
|
|
6571
|
+
const existingRuntime = this._runtimeTargetsForEquivalentInstances(editTarget.instanceId);
|
|
6572
|
+
if (existingRuntime.length > 0) {
|
|
6573
|
+
const roles = this._rolesForEquivalentInstances(editTarget.instanceId);
|
|
6574
|
+
return this._textResult({
|
|
6575
|
+
success: false,
|
|
6576
|
+
error: "Multiplayer playtest already running.",
|
|
6577
|
+
message: "A Studio runtime is already connected for this place. Close the existing playtest windows manually before starting another multiplayer playtest.",
|
|
6578
|
+
ready: true,
|
|
6579
|
+
timedOut: false,
|
|
6580
|
+
roles,
|
|
6581
|
+
runtimeRoles: existingRuntime.map((target) => target.role),
|
|
6582
|
+
manualCleanupRequired: true
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6585
|
+
const startedAt = Date.now();
|
|
5912
6586
|
const response = await this.client.request("/api/multiplayer-test-start", { numPlayers, testArgs: testArgs ?? {} }, editTarget.instanceId, editTarget.role);
|
|
5913
6587
|
if (response?.error) {
|
|
5914
6588
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
5915
6589
|
}
|
|
5916
|
-
const wait = await this._waitForMultiplayerStart(editTarget.instanceId, numPlayers, timeout ??
|
|
6590
|
+
const wait = await this._waitForMultiplayerStart(editTarget.instanceId, numPlayers, timeout ?? 60, startedAt);
|
|
6591
|
+
const launched = wait.ok;
|
|
5917
6592
|
const state = await this._buildMultiplayerState(editTarget.instanceId);
|
|
6593
|
+
const success = response.success === true && wait.ok;
|
|
5918
6594
|
return {
|
|
5919
6595
|
content: [{
|
|
5920
6596
|
type: "text",
|
|
5921
6597
|
text: JSON.stringify({
|
|
5922
6598
|
...response,
|
|
6599
|
+
success,
|
|
5923
6600
|
ready: wait.ok,
|
|
6601
|
+
launched,
|
|
6602
|
+
startRequested: response.success === true,
|
|
5924
6603
|
timedOut: wait.timedOut,
|
|
5925
6604
|
wait,
|
|
5926
6605
|
roles: wait.roles,
|
|
5927
|
-
state
|
|
6606
|
+
state,
|
|
6607
|
+
error: success ? void 0 : wait.error ?? "multiplayer_start_not_detected",
|
|
6608
|
+
message: success ? "Multiplayer Studio test started and runtime peers detected." : "Multiplayer Studio test start was requested, but MCP did not detect the required server/client peers before timeout. Close the multiplayer test windows manually if Studio launched them.",
|
|
6609
|
+
manualCleanupRequired: success || response.success === true ? true : void 0,
|
|
6610
|
+
startedAt
|
|
5928
6611
|
})
|
|
5929
6612
|
}]
|
|
5930
6613
|
};
|
|
@@ -5985,27 +6668,10 @@ ${code}`
|
|
|
5985
6668
|
};
|
|
5986
6669
|
}
|
|
5987
6670
|
async multiplayerTestEnd(value, timeout, instance_id) {
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
}
|
|
5993
|
-
const editDone = await this._waitForMultiplayerEditDone(serverTarget.instanceId, timeout ?? 30);
|
|
5994
|
-
const wait = await this._waitForRuntimeRoles(serverTarget.instanceId, { noRuntime: true }, timeout ?? 30);
|
|
5995
|
-
const state = await this._buildMultiplayerState(serverTarget.instanceId);
|
|
5996
|
-
return {
|
|
5997
|
-
content: [{
|
|
5998
|
-
type: "text",
|
|
5999
|
-
text: JSON.stringify({
|
|
6000
|
-
...response,
|
|
6001
|
-
ended: wait.ok,
|
|
6002
|
-
editDone,
|
|
6003
|
-
timedOut: wait.timedOut,
|
|
6004
|
-
roles: wait.roles,
|
|
6005
|
-
state
|
|
6006
|
-
})
|
|
6007
|
-
}]
|
|
6008
|
-
};
|
|
6671
|
+
void value;
|
|
6672
|
+
void timeout;
|
|
6673
|
+
void instance_id;
|
|
6674
|
+
return this._textResult(multiplayerStopDisabledBody());
|
|
6009
6675
|
}
|
|
6010
6676
|
async getConnectedInstances() {
|
|
6011
6677
|
const instances = this.bridge.getPublicInstances();
|
|
@@ -6041,14 +6707,14 @@ ${code}`
|
|
|
6041
6707
|
};
|
|
6042
6708
|
}
|
|
6043
6709
|
static findProjectRoot(startDir) {
|
|
6044
|
-
let dir =
|
|
6710
|
+
let dir = path4.resolve(startDir);
|
|
6045
6711
|
let previous = "";
|
|
6046
6712
|
while (dir !== previous) {
|
|
6047
|
-
if (
|
|
6713
|
+
if (fs3.existsSync(path4.join(dir, ".git")) || fs3.existsSync(path4.join(dir, "package.json"))) {
|
|
6048
6714
|
return dir;
|
|
6049
6715
|
}
|
|
6050
6716
|
previous = dir;
|
|
6051
|
-
dir =
|
|
6717
|
+
dir = path4.dirname(dir);
|
|
6052
6718
|
}
|
|
6053
6719
|
return null;
|
|
6054
6720
|
}
|
|
@@ -6056,15 +6722,15 @@ ${code}`
|
|
|
6056
6722
|
if (!candidate)
|
|
6057
6723
|
return false;
|
|
6058
6724
|
try {
|
|
6059
|
-
return
|
|
6725
|
+
return fs3.statSync(candidate).isDirectory();
|
|
6060
6726
|
} catch {
|
|
6061
6727
|
return false;
|
|
6062
6728
|
}
|
|
6063
6729
|
}
|
|
6064
6730
|
static ensureWritableDirectory(candidate, label) {
|
|
6065
|
-
const resolved =
|
|
6731
|
+
const resolved = path4.resolve(candidate);
|
|
6066
6732
|
try {
|
|
6067
|
-
|
|
6733
|
+
fs3.mkdirSync(resolved, { recursive: true });
|
|
6068
6734
|
} catch (error) {
|
|
6069
6735
|
throw new Error(`Unable to create ${label} build-library directory at ${resolved}: ${error.message}`);
|
|
6070
6736
|
}
|
|
@@ -6072,7 +6738,7 @@ ${code}`
|
|
|
6072
6738
|
throw new Error(`${label} build-library path is not a directory: ${resolved}`);
|
|
6073
6739
|
}
|
|
6074
6740
|
try {
|
|
6075
|
-
|
|
6741
|
+
fs3.accessSync(resolved, fs3.constants.W_OK);
|
|
6076
6742
|
} catch (error) {
|
|
6077
6743
|
throw new Error(`${label} build-library directory is not writable: ${resolved}. ${error.message}`);
|
|
6078
6744
|
}
|
|
@@ -6083,25 +6749,25 @@ ${code}`
|
|
|
6083
6749
|
if (_RobloxStudioTools._cachedLibraryPath)
|
|
6084
6750
|
return _RobloxStudioTools._cachedLibraryPath;
|
|
6085
6751
|
const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
|
|
6086
|
-
const cwd =
|
|
6752
|
+
const cwd = path4.resolve(process.cwd());
|
|
6087
6753
|
const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
|
|
6088
|
-
const homeLibraryPath =
|
|
6089
|
-
const projectLibraryPath = projectRoot ?
|
|
6090
|
-
const cwdLibraryPath =
|
|
6754
|
+
const homeLibraryPath = path4.join(os3.homedir(), ".robloxstudio-mcp", "build-library");
|
|
6755
|
+
const projectLibraryPath = projectRoot ? path4.join(projectRoot, "build-library") : null;
|
|
6756
|
+
const cwdLibraryPath = path4.join(cwd, "build-library");
|
|
6091
6757
|
let result;
|
|
6092
6758
|
if (overridePath) {
|
|
6093
6759
|
result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
|
|
6094
6760
|
} else {
|
|
6095
6761
|
const existing = [projectLibraryPath, cwdLibraryPath].find((c) => c && _RobloxStudioTools.isDirectory(c) && (() => {
|
|
6096
6762
|
try {
|
|
6097
|
-
|
|
6763
|
+
fs3.accessSync(c, fs3.constants.W_OK);
|
|
6098
6764
|
return true;
|
|
6099
6765
|
} catch {
|
|
6100
6766
|
return false;
|
|
6101
6767
|
}
|
|
6102
6768
|
})());
|
|
6103
6769
|
if (existing) {
|
|
6104
|
-
result =
|
|
6770
|
+
result = path4.resolve(existing);
|
|
6105
6771
|
} else if (projectLibraryPath) {
|
|
6106
6772
|
try {
|
|
6107
6773
|
result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
|
|
@@ -6128,12 +6794,12 @@ ${code}`
|
|
|
6128
6794
|
if (response && response.success && response.buildData) {
|
|
6129
6795
|
const buildData = response.buildData;
|
|
6130
6796
|
const buildId = buildData.id || `${style}/exported`;
|
|
6131
|
-
const filePath =
|
|
6132
|
-
const dirPath =
|
|
6133
|
-
if (!
|
|
6134
|
-
|
|
6797
|
+
const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
|
|
6798
|
+
const dirPath = path4.dirname(filePath);
|
|
6799
|
+
if (!fs3.existsSync(dirPath)) {
|
|
6800
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
6135
6801
|
}
|
|
6136
|
-
|
|
6802
|
+
fs3.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
|
|
6137
6803
|
response.savedTo = filePath;
|
|
6138
6804
|
}
|
|
6139
6805
|
return {
|
|
@@ -6230,12 +6896,12 @@ ${code}`
|
|
|
6230
6896
|
const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
|
|
6231
6897
|
const computedBounds = bounds || this.computeBounds(normalizedParts);
|
|
6232
6898
|
const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
|
|
6233
|
-
const filePath =
|
|
6234
|
-
const dirPath =
|
|
6235
|
-
if (!
|
|
6236
|
-
|
|
6899
|
+
const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
6900
|
+
const dirPath = path4.dirname(filePath);
|
|
6901
|
+
if (!fs3.existsSync(dirPath)) {
|
|
6902
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
6237
6903
|
}
|
|
6238
|
-
|
|
6904
|
+
fs3.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
|
|
6239
6905
|
return {
|
|
6240
6906
|
content: [
|
|
6241
6907
|
{
|
|
@@ -6289,12 +6955,12 @@ ${code}`
|
|
|
6289
6955
|
};
|
|
6290
6956
|
if (seed !== void 0)
|
|
6291
6957
|
buildData.generatorSeed = seed;
|
|
6292
|
-
const filePath =
|
|
6293
|
-
const dirPath =
|
|
6294
|
-
if (!
|
|
6295
|
-
|
|
6958
|
+
const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
6959
|
+
const dirPath = path4.dirname(filePath);
|
|
6960
|
+
if (!fs3.existsSync(dirPath)) {
|
|
6961
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
6296
6962
|
}
|
|
6297
|
-
|
|
6963
|
+
fs3.writeFileSync(filePath, JSON.stringify(buildData, null, 2));
|
|
6298
6964
|
return {
|
|
6299
6965
|
content: [
|
|
6300
6966
|
{
|
|
@@ -6318,17 +6984,17 @@ ${code}`
|
|
|
6318
6984
|
}
|
|
6319
6985
|
let resolved;
|
|
6320
6986
|
if (typeof buildData === "string") {
|
|
6321
|
-
const filePath =
|
|
6322
|
-
if (!
|
|
6987
|
+
const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
|
|
6988
|
+
if (!fs3.existsSync(filePath)) {
|
|
6323
6989
|
throw new Error(`Build not found in library: ${buildData}`);
|
|
6324
6990
|
}
|
|
6325
|
-
resolved = JSON.parse(
|
|
6991
|
+
resolved = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
6326
6992
|
} else if (buildData.id && !buildData.parts) {
|
|
6327
|
-
const filePath =
|
|
6328
|
-
if (!
|
|
6993
|
+
const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
|
|
6994
|
+
if (!fs3.existsSync(filePath)) {
|
|
6329
6995
|
throw new Error(`Build not found in library: ${buildData.id}`);
|
|
6330
6996
|
}
|
|
6331
|
-
resolved = JSON.parse(
|
|
6997
|
+
resolved = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
6332
6998
|
} else {
|
|
6333
6999
|
resolved = buildData;
|
|
6334
7000
|
}
|
|
@@ -6351,13 +7017,13 @@ ${code}`
|
|
|
6351
7017
|
const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
|
|
6352
7018
|
const builds = [];
|
|
6353
7019
|
for (const s of styles) {
|
|
6354
|
-
const dirPath =
|
|
6355
|
-
if (!
|
|
7020
|
+
const dirPath = path4.join(libraryPath, s);
|
|
7021
|
+
if (!fs3.existsSync(dirPath))
|
|
6356
7022
|
continue;
|
|
6357
|
-
const files =
|
|
7023
|
+
const files = fs3.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
|
|
6358
7024
|
for (const file of files) {
|
|
6359
7025
|
try {
|
|
6360
|
-
const content =
|
|
7026
|
+
const content = fs3.readFileSync(path4.join(dirPath, file), "utf-8");
|
|
6361
7027
|
const data = JSON.parse(content);
|
|
6362
7028
|
builds.push({
|
|
6363
7029
|
id: data.id || `${s}/${file.replace(".json", "")}`,
|
|
@@ -6396,11 +7062,11 @@ ${code}`
|
|
|
6396
7062
|
if (!id) {
|
|
6397
7063
|
throw new Error("Build ID is required for get_build");
|
|
6398
7064
|
}
|
|
6399
|
-
const filePath =
|
|
6400
|
-
if (!
|
|
7065
|
+
const filePath = path4.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
7066
|
+
if (!fs3.existsSync(filePath)) {
|
|
6401
7067
|
throw new Error(`Build not found in library: ${id}`);
|
|
6402
7068
|
}
|
|
6403
|
-
const data = JSON.parse(
|
|
7069
|
+
const data = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
6404
7070
|
const result = {
|
|
6405
7071
|
id: data.id,
|
|
6406
7072
|
style: data.style,
|
|
@@ -6483,11 +7149,11 @@ ${code}`
|
|
|
6483
7149
|
if (!buildId) {
|
|
6484
7150
|
throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
|
|
6485
7151
|
}
|
|
6486
|
-
const filePath =
|
|
6487
|
-
if (!
|
|
7152
|
+
const filePath = path4.join(libraryPath, `${buildId}.json`);
|
|
7153
|
+
if (!fs3.existsSync(filePath)) {
|
|
6488
7154
|
throw new Error(`Build not found in library: ${buildId}`);
|
|
6489
7155
|
}
|
|
6490
|
-
const content =
|
|
7156
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
6491
7157
|
const buildData = JSON.parse(content);
|
|
6492
7158
|
const buildName = buildId.split("/").pop() || buildId;
|
|
6493
7159
|
expandedBuilds.push({
|
|
@@ -6827,11 +7493,11 @@ ${code}`
|
|
|
6827
7493
|
return this.resolveUploadedReferenceImageId(decalId, instance_id);
|
|
6828
7494
|
}
|
|
6829
7495
|
async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
|
|
6830
|
-
if (!
|
|
7496
|
+
if (!fs3.existsSync(filePath)) {
|
|
6831
7497
|
throw new Error(`File not found: ${filePath}`);
|
|
6832
7498
|
}
|
|
6833
|
-
const fileContent =
|
|
6834
|
-
const fileName =
|
|
7499
|
+
const fileContent = fs3.readFileSync(filePath);
|
|
7500
|
+
const fileName = path4.basename(filePath);
|
|
6835
7501
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
6836
7502
|
const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
|
|
6837
7503
|
return {
|
|
@@ -7056,10 +7722,10 @@ ${code}`
|
|
|
7056
7722
|
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
7057
7723
|
}
|
|
7058
7724
|
const bytes = Buffer.from(response.base64, "base64");
|
|
7059
|
-
const resolved =
|
|
7725
|
+
const resolved = path4.resolve(outputPath);
|
|
7060
7726
|
try {
|
|
7061
|
-
|
|
7062
|
-
|
|
7727
|
+
fs3.mkdirSync(path4.dirname(resolved), { recursive: true });
|
|
7728
|
+
fs3.writeFileSync(resolved, bytes);
|
|
7063
7729
|
} catch (err) {
|
|
7064
7730
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err.message}` }) }] };
|
|
7065
7731
|
}
|
|
@@ -7092,9 +7758,9 @@ ${code}`
|
|
|
7092
7758
|
let bytes;
|
|
7093
7759
|
let sourceLabel;
|
|
7094
7760
|
if (source.path !== void 0) {
|
|
7095
|
-
const resolved =
|
|
7761
|
+
const resolved = path4.resolve(source.path);
|
|
7096
7762
|
try {
|
|
7097
|
-
bytes =
|
|
7763
|
+
bytes = fs3.readFileSync(resolved);
|
|
7098
7764
|
} catch (err) {
|
|
7099
7765
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err.message}` }) }] };
|
|
7100
7766
|
}
|
|
@@ -7317,7 +7983,7 @@ var init_proxy_bridge_service = __esm({
|
|
|
7317
7983
|
// ../core/dist/server.js
|
|
7318
7984
|
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7319
7985
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7320
|
-
import { CallToolRequestSchema as CallToolRequestSchema2, ErrorCode as
|
|
7986
|
+
import { CallToolRequestSchema as CallToolRequestSchema2, ErrorCode as ErrorCode3, ListToolsRequestSchema as ListToolsRequestSchema2, McpError as McpError3 } from "@modelcontextprotocol/sdk/types.js";
|
|
7321
7987
|
var RobloxStudioMCPServer;
|
|
7322
7988
|
var init_server = __esm({
|
|
7323
7989
|
"../core/dist/server.js"() {
|
|
@@ -7326,6 +7992,7 @@ var init_server = __esm({
|
|
|
7326
7992
|
init_tools();
|
|
7327
7993
|
init_bridge_service();
|
|
7328
7994
|
init_proxy_bridge_service();
|
|
7995
|
+
init_mcp_compat();
|
|
7329
7996
|
RobloxStudioMCPServer = class {
|
|
7330
7997
|
server;
|
|
7331
7998
|
tools;
|
|
@@ -7345,6 +8012,7 @@ var init_server = __esm({
|
|
|
7345
8012
|
});
|
|
7346
8013
|
this.bridge = new BridgeService();
|
|
7347
8014
|
this.tools = new RobloxStudioTools(this.bridge);
|
|
8015
|
+
registerEmptyResourceShim(this.server);
|
|
7348
8016
|
this.setupToolHandlers();
|
|
7349
8017
|
}
|
|
7350
8018
|
setupToolHandlers() {
|
|
@@ -7360,11 +8028,11 @@ var init_server = __esm({
|
|
|
7360
8028
|
this.server.setRequestHandler(CallToolRequestSchema2, async (request) => {
|
|
7361
8029
|
const { name, arguments: args } = request.params;
|
|
7362
8030
|
if (!this.allowedToolNames.has(name)) {
|
|
7363
|
-
throw new
|
|
8031
|
+
throw new McpError3(ErrorCode3.MethodNotFound, `Unknown tool: ${name}`);
|
|
7364
8032
|
}
|
|
7365
8033
|
const handler = TOOL_HANDLERS[name];
|
|
7366
8034
|
if (!handler) {
|
|
7367
|
-
throw new
|
|
8035
|
+
throw new McpError3(ErrorCode3.MethodNotFound, `Unknown tool: ${name}`);
|
|
7368
8036
|
}
|
|
7369
8037
|
try {
|
|
7370
8038
|
return await handler(this.tools, args ?? {});
|
|
@@ -7382,9 +8050,9 @@ var init_server = __esm({
|
|
|
7382
8050
|
isError: true
|
|
7383
8051
|
};
|
|
7384
8052
|
}
|
|
7385
|
-
if (error instanceof
|
|
8053
|
+
if (error instanceof McpError3)
|
|
7386
8054
|
throw error;
|
|
7387
|
-
throw new
|
|
8055
|
+
throw new McpError3(ErrorCode3.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7388
8056
|
}
|
|
7389
8057
|
});
|
|
7390
8058
|
}
|
|
@@ -8049,7 +8717,7 @@ var init_definitions = __esm({
|
|
|
8049
8717
|
{
|
|
8050
8718
|
name: "get_script_source",
|
|
8051
8719
|
category: "read",
|
|
8052
|
-
description: 'Get script source. Returns "source" and "numberedSource" (line-numbered).
|
|
8720
|
+
description: 'Get script source. Returns "source" and "numberedSource" (line-numbered). Pass line_range for large scripts; without a range, large scripts are truncated (see the "truncated" flag and "note") to avoid flooding the context.',
|
|
8053
8721
|
inputSchema: {
|
|
8054
8722
|
type: "object",
|
|
8055
8723
|
properties: {
|
|
@@ -8057,13 +8725,9 @@ var init_definitions = __esm({
|
|
|
8057
8725
|
type: "string",
|
|
8058
8726
|
description: "Canonical path to a LuaSourceContainer"
|
|
8059
8727
|
},
|
|
8060
|
-
|
|
8061
|
-
type: "
|
|
8062
|
-
description: "
|
|
8063
|
-
},
|
|
8064
|
-
endLine: {
|
|
8065
|
-
type: "number",
|
|
8066
|
-
description: "End line (inclusive)"
|
|
8728
|
+
line_range: {
|
|
8729
|
+
type: "string",
|
|
8730
|
+
description: 'Line range to return: "start-end" (e.g. "100-200"), open-ended ("100-" or "-200"), or a single line ("42").'
|
|
8067
8731
|
},
|
|
8068
8732
|
instance_id: {
|
|
8069
8733
|
type: "string",
|
|
@@ -8099,7 +8763,7 @@ var init_definitions = __esm({
|
|
|
8099
8763
|
{
|
|
8100
8764
|
name: "edit_script_lines",
|
|
8101
8765
|
category: "write",
|
|
8102
|
-
description:
|
|
8766
|
+
description: 'Replace exact text in a script. Without line_range, old_string must match exactly once in the script. Pass line_range as a single line (e.g. "42") to anchor the edit when old_string is ambiguous.',
|
|
8103
8767
|
inputSchema: {
|
|
8104
8768
|
type: "object",
|
|
8105
8769
|
properties: {
|
|
@@ -8109,15 +8773,15 @@ var init_definitions = __esm({
|
|
|
8109
8773
|
},
|
|
8110
8774
|
old_string: {
|
|
8111
8775
|
type: "string",
|
|
8112
|
-
description: "Exact text to find and replace. Must be unique in the script unless
|
|
8776
|
+
description: "Exact text to find and replace. Must be unique in the script unless line_range is provided."
|
|
8113
8777
|
},
|
|
8114
8778
|
new_string: {
|
|
8115
8779
|
type: "string",
|
|
8116
8780
|
description: "Replacement text"
|
|
8117
8781
|
},
|
|
8118
|
-
|
|
8119
|
-
type: "
|
|
8120
|
-
description:
|
|
8782
|
+
line_range: {
|
|
8783
|
+
type: "string",
|
|
8784
|
+
description: 'Optional single line where old_string begins, such as "42". When provided, skips uniqueness check and requires old_string to match starting at that exact line.'
|
|
8121
8785
|
},
|
|
8122
8786
|
instance_id: {
|
|
8123
8787
|
type: "string",
|
|
@@ -8157,7 +8821,7 @@ var init_definitions = __esm({
|
|
|
8157
8821
|
{
|
|
8158
8822
|
name: "delete_script_lines",
|
|
8159
8823
|
category: "write",
|
|
8160
|
-
description: "Delete a range of lines. 1-indexed
|
|
8824
|
+
description: "Delete a range of lines. line_range is 1-indexed and inclusive.",
|
|
8161
8825
|
inputSchema: {
|
|
8162
8826
|
type: "object",
|
|
8163
8827
|
properties: {
|
|
@@ -8165,20 +8829,16 @@ var init_definitions = __esm({
|
|
|
8165
8829
|
type: "string",
|
|
8166
8830
|
description: "Canonical path to a LuaSourceContainer"
|
|
8167
8831
|
},
|
|
8168
|
-
|
|
8169
|
-
type: "
|
|
8170
|
-
description: "
|
|
8171
|
-
},
|
|
8172
|
-
endLine: {
|
|
8173
|
-
type: "number",
|
|
8174
|
-
description: "End line (inclusive)"
|
|
8832
|
+
line_range: {
|
|
8833
|
+
type: "string",
|
|
8834
|
+
description: 'Line range to delete: "start-end" (e.g. "100-200") or a single line ("42"). Open-ended ranges are not accepted for deletion.'
|
|
8175
8835
|
},
|
|
8176
8836
|
instance_id: {
|
|
8177
8837
|
type: "string",
|
|
8178
8838
|
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
8179
8839
|
}
|
|
8180
8840
|
},
|
|
8181
|
-
required: ["instancePath", "
|
|
8841
|
+
required: ["instancePath", "line_range"]
|
|
8182
8842
|
}
|
|
8183
8843
|
},
|
|
8184
8844
|
// === Attributes ===
|
|
@@ -8424,21 +9084,21 @@ var init_definitions = __esm({
|
|
|
8424
9084
|
{
|
|
8425
9085
|
name: "grep_scripts",
|
|
8426
9086
|
category: "read",
|
|
8427
|
-
description:
|
|
9087
|
+
description: 'Ripgrep-inspired search across all script sources. Supports literal and Lua pattern matching (with top-level "|" alternation), context lines, early termination, and results grouped by script with line/column numbers.',
|
|
8428
9088
|
inputSchema: {
|
|
8429
9089
|
type: "object",
|
|
8430
9090
|
properties: {
|
|
8431
9091
|
pattern: {
|
|
8432
9092
|
type: "string",
|
|
8433
|
-
description:
|
|
9093
|
+
description: 'Search text. Literal by default; with usePattern=true it is a case-sensitive Lua pattern with top-level "|" alternation (e.g. "foo|bar").'
|
|
8434
9094
|
},
|
|
8435
9095
|
caseSensitive: {
|
|
8436
9096
|
type: "boolean",
|
|
8437
|
-
description: "
|
|
9097
|
+
description: "Literal search case sensitivity (default: false). Lua pattern mode is always case-sensitive; passing false with usePattern=true is rejected."
|
|
8438
9098
|
},
|
|
8439
9099
|
usePattern: {
|
|
8440
9100
|
type: "boolean",
|
|
8441
|
-
description:
|
|
9101
|
+
description: 'Use case-sensitive Lua pattern matching instead of literal search (default: false). Supports top-level alternation: "a|b" matches a line containing "a" or "b". Note: Lua patterns are NOT PCRE \u2014 use %d/%a/%w classes and ".-" (not ".*?"); ^ $ ( ) . % + - * ? [ ] are magic.'
|
|
8442
9102
|
},
|
|
8443
9103
|
contextLines: {
|
|
8444
9104
|
type: "number",
|
|
@@ -8477,7 +9137,7 @@ var init_definitions = __esm({
|
|
|
8477
9137
|
{
|
|
8478
9138
|
name: "manage_instance",
|
|
8479
9139
|
category: "write",
|
|
8480
|
-
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.',
|
|
9140
|
+
description: 'Launch, close, inspect, and find revisions for Studio instances. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, 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.',
|
|
8481
9141
|
inputSchema: {
|
|
8482
9142
|
type: "object",
|
|
8483
9143
|
properties: {
|
|
@@ -8489,7 +9149,7 @@ var init_definitions = __esm({
|
|
|
8489
9149
|
source: {
|
|
8490
9150
|
type: "string",
|
|
8491
9151
|
enum: ["baseplate", "local_file", "published_place", "place_revision"],
|
|
8492
|
-
description: 'Required for action="launch". published_place opens the latest place; place_revision opens a specific older version as an anonymous local copy.'
|
|
9152
|
+
description: 'Required for action="launch". baseplate/local_file do not use place_id; published_place opens the latest place; place_revision opens a specific older version as an anonymous local copy.'
|
|
8493
9153
|
},
|
|
8494
9154
|
local_place_file: {
|
|
8495
9155
|
type: "string",
|
|
@@ -8497,11 +9157,7 @@ var init_definitions = __esm({
|
|
|
8497
9157
|
},
|
|
8498
9158
|
place_id: {
|
|
8499
9159
|
type: "number",
|
|
8500
|
-
description: '
|
|
8501
|
-
},
|
|
8502
|
-
universe_id: {
|
|
8503
|
-
type: "number",
|
|
8504
|
-
description: "Optional for published_place/place_revision launches; derived from place_id when omitted."
|
|
9160
|
+
description: 'Only used for source="published_place", source="place_revision", and action="list_place_versions". Do not pass for source="baseplate" or source="local_file".'
|
|
8505
9161
|
},
|
|
8506
9162
|
place_version: {
|
|
8507
9163
|
type: "number",
|
|
@@ -8838,7 +9494,7 @@ var init_definitions = __esm({
|
|
|
8838
9494
|
{
|
|
8839
9495
|
name: "multiplayer_playtest",
|
|
8840
9496
|
category: "write",
|
|
8841
|
-
description: 'Start
|
|
9497
|
+
description: 'Start or inspect a StudioTestService multiplayer playtest. Use action="start" with numPlayers and force=true only when you accept that MCP cannot stop it and you must manually close the multiplayer test windows afterward. action="status" inspects state, action="add_players" adds players, and action="leave_client" removes one client. action="end" is disabled for now and returns the StudioTestService:EndTest broken-API reason. Returns brief lifecycle status only; read script output with get_runtime_logs.',
|
|
8842
9498
|
inputSchema: {
|
|
8843
9499
|
type: "object",
|
|
8844
9500
|
properties: {
|
|
@@ -8859,11 +9515,15 @@ var init_definitions = __esm({
|
|
|
8859
9515
|
description: 'For action="start": JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients.'
|
|
8860
9516
|
},
|
|
8861
9517
|
value: {
|
|
8862
|
-
description: '
|
|
9518
|
+
description: 'Ignored while action="end" is disabled.'
|
|
9519
|
+
},
|
|
9520
|
+
force: {
|
|
9521
|
+
type: "boolean",
|
|
9522
|
+
description: 'Required for action="start". Pass true only if you understand StudioTestService:EndTest is broken in this flow and you will manually close the multiplayer test windows.'
|
|
8863
9523
|
},
|
|
8864
9524
|
timeout: {
|
|
8865
9525
|
type: "number",
|
|
8866
|
-
description: "Max seconds to wait for action completion. Defaults to 30."
|
|
9526
|
+
description: "Max seconds to wait for start peer detection or action completion. Defaults to 30."
|
|
8867
9527
|
},
|
|
8868
9528
|
instance_id: {
|
|
8869
9529
|
type: "string",
|
|
@@ -10143,7 +10803,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10143
10803
|
{
|
|
10144
10804
|
name: "multiplayer_test_start",
|
|
10145
10805
|
category: "write",
|
|
10146
|
-
description: 'Deprecated. Use multiplayer_playtest with action="start" instead. Starts a StudioTestService multiplayer test.',
|
|
10806
|
+
description: 'Deprecated. Use multiplayer_playtest with action="start" instead. Starts a StudioTestService multiplayer test only when force=true acknowledges that MCP cannot stop it and the test windows must be closed manually.',
|
|
10147
10807
|
inputSchema: {
|
|
10148
10808
|
type: "object",
|
|
10149
10809
|
properties: {
|
|
@@ -10154,6 +10814,10 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10154
10814
|
testArgs: {
|
|
10155
10815
|
description: "JSON-compatible table passed to StudioTestService:GetTestArgs() on server and clients."
|
|
10156
10816
|
},
|
|
10817
|
+
force: {
|
|
10818
|
+
type: "boolean",
|
|
10819
|
+
description: "Required. Pass true only if you understand StudioTestService:EndTest is broken in this flow and you will manually close the multiplayer test windows."
|
|
10820
|
+
},
|
|
10157
10821
|
timeout: {
|
|
10158
10822
|
type: "number",
|
|
10159
10823
|
description: "Max seconds to wait for server + clients to register (default 30)."
|
|
@@ -10163,7 +10827,7 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10163
10827
|
description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
|
|
10164
10828
|
}
|
|
10165
10829
|
},
|
|
10166
|
-
required: ["numPlayers"]
|
|
10830
|
+
required: ["numPlayers", "force"]
|
|
10167
10831
|
}
|
|
10168
10832
|
},
|
|
10169
10833
|
{
|
|
@@ -10228,16 +10892,16 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10228
10892
|
{
|
|
10229
10893
|
name: "multiplayer_test_end",
|
|
10230
10894
|
category: "write",
|
|
10231
|
-
description:
|
|
10895
|
+
description: "Deprecated. Multiplayer StudioTestService stop/end is disabled for now because StudioTestService:EndTest is broken in this flow. This tool returns a disabled error and does not call EndTest.",
|
|
10232
10896
|
inputSchema: {
|
|
10233
10897
|
type: "object",
|
|
10234
10898
|
properties: {
|
|
10235
10899
|
value: {
|
|
10236
|
-
description: "
|
|
10900
|
+
description: "Ignored while multiplayer stop/end is disabled."
|
|
10237
10901
|
},
|
|
10238
10902
|
timeout: {
|
|
10239
10903
|
type: "number",
|
|
10240
|
-
description: "
|
|
10904
|
+
description: "Ignored while multiplayer stop/end is disabled."
|
|
10241
10905
|
},
|
|
10242
10906
|
instance_id: {
|
|
10243
10907
|
type: "string",
|
|
@@ -10253,15 +10917,15 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10253
10917
|
});
|
|
10254
10918
|
|
|
10255
10919
|
// ../core/dist/install-plugin-helpers.js
|
|
10256
|
-
import { existsSync as existsSync4, readFileSync as
|
|
10920
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
10257
10921
|
import { execSync } from "child_process";
|
|
10258
|
-
import { join as
|
|
10259
|
-
import { homedir as
|
|
10922
|
+
import { join as join4 } from "path";
|
|
10923
|
+
import { homedir as homedir4 } from "os";
|
|
10260
10924
|
function isWSL() {
|
|
10261
10925
|
if (process.platform !== "linux")
|
|
10262
10926
|
return false;
|
|
10263
10927
|
try {
|
|
10264
|
-
const v =
|
|
10928
|
+
const v = readFileSync5("/proc/version", "utf8");
|
|
10265
10929
|
return /microsoft|wsl/i.test(v);
|
|
10266
10930
|
} catch {
|
|
10267
10931
|
return false;
|
|
@@ -10279,7 +10943,7 @@ function getWindowsUserPluginsDir() {
|
|
|
10279
10943
|
}).toString().trim();
|
|
10280
10944
|
if (!linuxPath)
|
|
10281
10945
|
return null;
|
|
10282
|
-
return
|
|
10946
|
+
return join4(linuxPath, "Roblox", "Plugins");
|
|
10283
10947
|
} catch {
|
|
10284
10948
|
return null;
|
|
10285
10949
|
}
|
|
@@ -10288,7 +10952,7 @@ function getPluginsFolder() {
|
|
|
10288
10952
|
if (process.env.MCP_PLUGINS_DIR)
|
|
10289
10953
|
return process.env.MCP_PLUGINS_DIR;
|
|
10290
10954
|
if (process.platform === "win32") {
|
|
10291
|
-
return
|
|
10955
|
+
return join4(process.env.LOCALAPPDATA || join4(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
|
|
10292
10956
|
}
|
|
10293
10957
|
if (isWSL()) {
|
|
10294
10958
|
const win = getWindowsUserPluginsDir();
|
|
@@ -10296,10 +10960,10 @@ function getPluginsFolder() {
|
|
|
10296
10960
|
return win;
|
|
10297
10961
|
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.");
|
|
10298
10962
|
}
|
|
10299
|
-
return
|
|
10963
|
+
return join4(homedir4(), "Documents", "Roblox", "Plugins");
|
|
10300
10964
|
}
|
|
10301
10965
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
10302
|
-
const otherDest =
|
|
10966
|
+
const otherDest = join4(pluginsFolder, otherAssetName);
|
|
10303
10967
|
if (!existsSync4(otherDest))
|
|
10304
10968
|
return;
|
|
10305
10969
|
if (replace) {
|
|
@@ -10345,8 +11009,8 @@ __export(install_plugin_exports, {
|
|
|
10345
11009
|
installBundledPlugin: () => installBundledPlugin,
|
|
10346
11010
|
installPlugin: () => installPlugin
|
|
10347
11011
|
});
|
|
10348
|
-
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as
|
|
10349
|
-
import { dirname as dirname3, join as
|
|
11012
|
+
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
|
|
11013
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
10350
11014
|
import { fileURLToPath } from "url";
|
|
10351
11015
|
import { get } from "https";
|
|
10352
11016
|
function httpsGet(url) {
|
|
@@ -10417,7 +11081,7 @@ function prepareInstall({
|
|
|
10417
11081
|
}) {
|
|
10418
11082
|
const pluginsFolder = getPluginsFolder();
|
|
10419
11083
|
if (!existsSync5(pluginsFolder)) {
|
|
10420
|
-
|
|
11084
|
+
mkdirSync4(pluginsFolder, { recursive: true });
|
|
10421
11085
|
}
|
|
10422
11086
|
handleVariantConflict({
|
|
10423
11087
|
pluginsFolder,
|
|
@@ -10431,21 +11095,21 @@ function prepareInstall({
|
|
|
10431
11095
|
function bundledAssetPath() {
|
|
10432
11096
|
const currentDir = dirname3(fileURLToPath(import.meta.url));
|
|
10433
11097
|
const candidates = [
|
|
10434
|
-
|
|
10435
|
-
|
|
11098
|
+
join5(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
11099
|
+
join5(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
10436
11100
|
];
|
|
10437
11101
|
return candidates.find((candidate) => existsSync5(candidate)) ?? null;
|
|
10438
11102
|
}
|
|
10439
11103
|
function packageVersion() {
|
|
10440
11104
|
const currentDir = dirname3(fileURLToPath(import.meta.url));
|
|
10441
|
-
const pkg = JSON.parse(
|
|
11105
|
+
const pkg = JSON.parse(readFileSync6(join5(currentDir, "..", "package.json"), "utf8"));
|
|
10442
11106
|
if (!pkg.version) {
|
|
10443
11107
|
throw new Error("Package version not found");
|
|
10444
11108
|
}
|
|
10445
11109
|
return pkg.version;
|
|
10446
11110
|
}
|
|
10447
11111
|
function bundledPluginVersion(source) {
|
|
10448
|
-
const match =
|
|
11112
|
+
const match = readFileSync6(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
10449
11113
|
return match ? match[1] : null;
|
|
10450
11114
|
}
|
|
10451
11115
|
function assertBundledPluginVersion(source) {
|
|
@@ -10459,8 +11123,8 @@ function assertBundledPluginVersion(source) {
|
|
|
10459
11123
|
}
|
|
10460
11124
|
function filesMatch(a, b) {
|
|
10461
11125
|
if (!existsSync5(b)) return false;
|
|
10462
|
-
const aBytes =
|
|
10463
|
-
const bBytes =
|
|
11126
|
+
const aBytes = readFileSync6(a);
|
|
11127
|
+
const bBytes = readFileSync6(b);
|
|
10464
11128
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
10465
11129
|
}
|
|
10466
11130
|
async function installBundledPlugin(options = {}) {
|
|
@@ -10473,7 +11137,7 @@ async function installBundledPlugin(options = {}) {
|
|
|
10473
11137
|
}
|
|
10474
11138
|
assertBundledPluginVersion(source);
|
|
10475
11139
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
10476
|
-
const dest =
|
|
11140
|
+
const dest = join5(pluginsFolder, ASSET_NAME);
|
|
10477
11141
|
if (filesMatch(source, dest)) return;
|
|
10478
11142
|
copyFileSync2(source, dest);
|
|
10479
11143
|
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
@@ -10487,7 +11151,7 @@ async function installPlugin(options = {}) {
|
|
|
10487
11151
|
const bundled = bundledAssetPath();
|
|
10488
11152
|
if (bundled) {
|
|
10489
11153
|
assertBundledPluginVersion(bundled);
|
|
10490
|
-
const dest2 =
|
|
11154
|
+
const dest2 = join5(pluginsFolder, ASSET_NAME);
|
|
10491
11155
|
if (filesMatch(bundled, dest2)) {
|
|
10492
11156
|
log(`${ASSET_NAME} already installed.`);
|
|
10493
11157
|
return;
|
|
@@ -10502,7 +11166,7 @@ async function installPlugin(options = {}) {
|
|
|
10502
11166
|
if (!asset) {
|
|
10503
11167
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
10504
11168
|
}
|
|
10505
|
-
const dest =
|
|
11169
|
+
const dest = join5(pluginsFolder, ASSET_NAME);
|
|
10506
11170
|
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
10507
11171
|
await download(asset.browser_download_url, dest);
|
|
10508
11172
|
log(`Installed to ${dest}`);
|