@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.
@@ -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
- let sourceToReturn = fullSource;
39
- let returnedStartLine = 1;
40
- let returnedEndLine = totalLineCount;
41
-
42
- if (startLine !== undefined || endLine !== undefined) {
43
- const actualStartLine = math.max(1, startLine ?? 1);
44
- const actualEndLine = math.min(lines.size(), endLine ?? lines.size());
45
-
46
- const selectedLines: string[] = [];
47
- for (let i = actualStartLine; i <= actualEndLine; i++) {
48
- selectedLines.push(lines[i - 1] ?? "");
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: startLine !== undefined || endLine !== undefined,
78
- truncated: false,
89
+ isPartial: explicitRange,
90
+ truncated: shouldTruncate,
79
91
  };
80
92
 
81
- if (startLine === undefined && endLine === undefined && fullSource.size() > 50000) {
82
- const truncatedLines: string[] = [];
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
- let topServiceInst: Instance = instance;
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(requestData: Record<string, unknown>) {
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: true,
337
- message: "Multiplayer Studio test end requested.",
338
- value,
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