@chrrxs/robloxstudio-mcp-inspector 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
|
@@ -6,6 +6,10 @@ const ScriptEditorService = game.GetService("ScriptEditorService");
|
|
|
6
6
|
const { getInstancePath, getInstanceByPath, readScriptSource, splitLines, joinLines } = Utils;
|
|
7
7
|
const { beginRecording, finishRecording } = Recording;
|
|
8
8
|
|
|
9
|
+
const SOURCE_TRUNCATE_CHAR_BUDGET = 25000;
|
|
10
|
+
const SOURCE_TRUNCATE_LINE_BUDGET = 400;
|
|
11
|
+
const SOURCE_TRUNCATE_TO_LINES = 300;
|
|
12
|
+
|
|
9
13
|
function normalizeEscapes(s: string): string {
|
|
10
14
|
let result = s;
|
|
11
15
|
result = result.gsub("\\\\", "\x01")[0];
|
|
@@ -17,6 +21,30 @@ function normalizeEscapes(s: string): string {
|
|
|
17
21
|
return result;
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
function getTopServiceName(instance: Instance): string {
|
|
25
|
+
let topServiceInst: Instance = instance;
|
|
26
|
+
while (topServiceInst.Parent && topServiceInst.Parent !== game) {
|
|
27
|
+
topServiceInst = topServiceInst.Parent;
|
|
28
|
+
}
|
|
29
|
+
return topServiceInst.Name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sliceLines(lines: string[], startLine: number, endLine: number): string[] {
|
|
33
|
+
const selectedLines: string[] = [];
|
|
34
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
35
|
+
selectedLines.push(lines[i - 1] ?? "");
|
|
36
|
+
}
|
|
37
|
+
return selectedLines;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function numberLines(lines: string[], lineOffset: number): string {
|
|
41
|
+
const numberedLines: string[] = [];
|
|
42
|
+
for (let i = 0; i < lines.size(); i++) {
|
|
43
|
+
numberedLines.push(`${i + lineOffset}: ${lines[i]}`);
|
|
44
|
+
}
|
|
45
|
+
return numberedLines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
function getScriptSource(requestData: Record<string, unknown>) {
|
|
21
49
|
const instancePath = requestData.instancePath as string;
|
|
22
50
|
const startLine = requestData.startLine as number | undefined;
|
|
@@ -34,74 +62,43 @@ function getScriptSource(requestData: Record<string, unknown>) {
|
|
|
34
62
|
const fullSource = readScriptSource(instance);
|
|
35
63
|
const [lines, hasTrailingNewline] = splitLines(fullSource);
|
|
36
64
|
const totalLineCount = lines.size();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
sourceToReturn = selectedLines.join("\n");
|
|
52
|
-
if (hasTrailingNewline && actualEndLine === lines.size() && sourceToReturn.sub(-1) !== "\n") {
|
|
53
|
-
sourceToReturn += "\n";
|
|
54
|
-
}
|
|
55
|
-
returnedStartLine = actualStartLine;
|
|
56
|
-
returnedEndLine = actualEndLine;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const numberedLines: string[] = [];
|
|
60
|
-
const linesToNumber = startLine !== undefined ? splitLines(sourceToReturn)[0] : lines;
|
|
61
|
-
const lineOffset = returnedStartLine - 1;
|
|
62
|
-
for (let i = 0; i < linesToNumber.size(); i++) {
|
|
63
|
-
numberedLines.push(`${i + 1 + lineOffset}: ${linesToNumber[i]}`);
|
|
64
|
-
}
|
|
65
|
-
const numberedSource = numberedLines.join("\n");
|
|
65
|
+
const explicitRange = startLine !== undefined || endLine !== undefined;
|
|
66
|
+
const shouldTruncate = !explicitRange &&
|
|
67
|
+
(fullSource.size() > SOURCE_TRUNCATE_CHAR_BUDGET || totalLineCount > SOURCE_TRUNCATE_LINE_BUDGET);
|
|
68
|
+
const returnedStartLine = explicitRange ? math.max(1, startLine ?? 1) : 1;
|
|
69
|
+
const returnedEndLine = shouldTruncate
|
|
70
|
+
? math.min(SOURCE_TRUNCATE_TO_LINES, totalLineCount)
|
|
71
|
+
: explicitRange ? math.min(totalLineCount, endLine ?? totalLineCount) : totalLineCount;
|
|
72
|
+
const selectedLines = (explicitRange || shouldTruncate)
|
|
73
|
+
? sliceLines(lines, returnedStartLine, returnedEndLine)
|
|
74
|
+
: lines;
|
|
75
|
+
const sourceToReturn = explicitRange
|
|
76
|
+
? joinLines(selectedLines, hasTrailingNewline && returnedEndLine === totalLineCount)
|
|
77
|
+
: shouldTruncate ? selectedLines.join("\n") : fullSource;
|
|
66
78
|
|
|
67
79
|
const resp: Record<string, unknown> = {
|
|
68
80
|
instancePath,
|
|
69
81
|
className: instance.ClassName,
|
|
70
82
|
name: instance.Name,
|
|
71
83
|
source: sourceToReturn,
|
|
72
|
-
numberedSource,
|
|
84
|
+
numberedSource: numberLines(selectedLines, returnedStartLine),
|
|
73
85
|
sourceLength: fullSource.size(),
|
|
74
86
|
lineCount: totalLineCount,
|
|
75
87
|
startLine: returnedStartLine,
|
|
76
88
|
endLine: returnedEndLine,
|
|
77
|
-
isPartial:
|
|
78
|
-
truncated:
|
|
89
|
+
isPartial: explicitRange,
|
|
90
|
+
truncated: shouldTruncate,
|
|
79
91
|
};
|
|
80
92
|
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
const truncatedNumberedLines: string[] = [];
|
|
84
|
-
const maxLines = math.min(1000, lines.size());
|
|
85
|
-
for (let i = 0; i < maxLines; i++) {
|
|
86
|
-
truncatedLines.push(lines[i]);
|
|
87
|
-
truncatedNumberedLines.push(`${i + 1}: ${lines[i]}`);
|
|
88
|
-
}
|
|
89
|
-
resp.source = truncatedLines.join("\n");
|
|
90
|
-
resp.numberedSource = truncatedNumberedLines.join("\n");
|
|
91
|
-
resp.truncated = true;
|
|
92
|
-
resp.endLine = maxLines;
|
|
93
|
-
resp.note = "Script truncated to first 1000 lines. Use startLine/endLine parameters to read specific sections.";
|
|
93
|
+
if (shouldTruncate) {
|
|
94
|
+
resp.note = `Script truncated to first ${returnedEndLine} of ${totalLineCount} lines (${fullSource.size()} chars). Use line_range to read specific sections.`;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
if (instance.IsA("BaseScript")) {
|
|
97
98
|
resp.enabled = instance.Enabled;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
while (topServiceInst.Parent && topServiceInst.Parent !== game) {
|
|
102
|
-
topServiceInst = topServiceInst.Parent;
|
|
103
|
-
}
|
|
104
|
-
resp.topService = topServiceInst.Name;
|
|
101
|
+
resp.topService = getTopServiceName(instance);
|
|
105
102
|
|
|
106
103
|
return resp;
|
|
107
104
|
});
|
|
@@ -322,20 +322,13 @@ function multiplayerTestLeaveClient(_requestData: Record<string, unknown>) {
|
|
|
322
322
|
};
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
function multiplayerTestEnd(
|
|
326
|
-
if (!RunService.IsRunning() || !RunService.IsServer()) {
|
|
327
|
-
return { error: "multiplayer_test_end must be called on the running server peer. Route with target=server." };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const value = requestData.value !== undefined ? requestData.value : "ended_by_mcp";
|
|
331
|
-
const [ok, result] = pcall(() => StudioTestService.EndTest(value));
|
|
332
|
-
if (!ok) {
|
|
333
|
-
return { error: tostring(result) };
|
|
334
|
-
}
|
|
325
|
+
function multiplayerTestEnd(_requestData: Record<string, unknown>) {
|
|
335
326
|
return {
|
|
336
|
-
success:
|
|
337
|
-
|
|
338
|
-
|
|
327
|
+
success: false,
|
|
328
|
+
error: "multiplayer_stop_disabled",
|
|
329
|
+
message: "Multiplayer playtest stop/end is disabled because StudioTestService:EndTest is currently broken for this flow. Manually close the Studio multiplayer test windows instead.",
|
|
330
|
+
reason: "StudioTestService:EndTest does not reliably end StudioTestService multiplayer sessions from MCP right now.",
|
|
331
|
+
manualCleanupRequired: true,
|
|
339
332
|
};
|
|
340
333
|
}
|
|
341
334
|
|
|
@@ -27,6 +27,8 @@ BreakpointHandlers.init(plugin);
|
|
|
27
27
|
ServerUrlSettings.init(plugin);
|
|
28
28
|
|
|
29
29
|
function applyRememberedServerUrl(): void {
|
|
30
|
+
if (ClientBroker.forkRole() === "client") return;
|
|
31
|
+
|
|
30
32
|
const rememberedServerUrl = ServerUrlSettings.readServerUrl();
|
|
31
33
|
if (rememberedServerUrl === undefined) return;
|
|
32
34
|
|