@chrrxs/robloxstudio-mcp-inspector 2.16.4 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,8 @@ import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
5
5
  import CaptureHandlers from "./handlers/CaptureHandlers";
6
6
  import InputHandlers from "./handlers/InputHandlers";
7
7
  import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
8
+ import BreakpointHandlers from "./handlers/BreakpointHandlers";
9
+ import ScriptProfilerHandlers from "./handlers/ScriptProfilerHandlers";
8
10
  import LuauExec from "./LuauExec";
9
11
  import State from "./State";
10
12
  import HttpDiagnostics from "./HttpDiagnostics";
@@ -94,6 +96,8 @@ const CLIENT_BROKER_ALLOWED_ENDPOINTS = new Set<string>([
94
96
  "/api/get-runtime-logs",
95
97
  "/api/get-memory-breakdown",
96
98
  "/api/get-scene-analysis",
99
+ "/api/breakpoints",
100
+ "/api/capture-script-profiler",
97
101
  "/api/multiplayer-test-state",
98
102
  "/api/multiplayer-test-leave-client",
99
103
  // Screenshot capture must run in the client peer (CaptureService captures
@@ -266,6 +270,12 @@ function setupClientBroker() {
266
270
  if (payload && payload.endpoint === "/api/get-scene-analysis") {
267
271
  return SceneAnalysisHandlers.getSceneAnalysis(payload.data ?? {});
268
272
  }
273
+ if (payload && payload.endpoint === "/api/breakpoints") {
274
+ return BreakpointHandlers.breakpoints(payload.data ?? {});
275
+ }
276
+ if (payload && payload.endpoint === "/api/capture-script-profiler") {
277
+ return ScriptProfilerHandlers.captureScriptProfiler(payload.data ?? {});
278
+ }
269
279
  if (payload && payload.endpoint === "/api/multiplayer-test-state") {
270
280
  return handleMultiplayerTestState();
271
281
  }
@@ -17,6 +17,8 @@ import LogHandlers from "./handlers/LogHandlers";
17
17
  import SerializationHandlers from "./handlers/SerializationHandlers";
18
18
  import MemoryHandlers from "./handlers/MemoryHandlers";
19
19
  import SceneAnalysisHandlers from "./handlers/SceneAnalysisHandlers";
20
+ import BreakpointHandlers from "./handlers/BreakpointHandlers";
21
+ import ScriptProfilerHandlers from "./handlers/ScriptProfilerHandlers";
20
22
  import EvalRuntimeHandlers from "./handlers/EvalRuntimeHandlers";
21
23
  import ClientBroker from "./ClientBroker";
22
24
  import ServerUrlSettings from "./ServerUrlSettings";
@@ -109,7 +111,6 @@ const routeMap: Record<string, Handler> = {
109
111
  "/api/grep-scripts": QueryHandlers.grepScripts,
110
112
  "/api/get-descendants": QueryHandlers.getDescendants,
111
113
  "/api/compare-instances": QueryHandlers.compareInstances,
112
- "/api/get-output-log": QueryHandlers.getOutputLog,
113
114
 
114
115
  "/api/set-property": PropertyHandlers.setProperty,
115
116
  "/api/set-properties": PropertyHandlers.setProperties,
@@ -146,7 +147,6 @@ const routeMap: Record<string, Handler> = {
146
147
 
147
148
  "/api/start-playtest": TestHandlers.startPlaytest,
148
149
  "/api/stop-playtest": TestHandlers.stopPlaytest,
149
- "/api/get-playtest-output": TestHandlers.getPlaytestOutput,
150
150
  "/api/multiplayer-test-start": TestHandlers.multiplayerTestStart,
151
151
  "/api/multiplayer-test-state": TestHandlers.multiplayerTestState,
152
152
  "/api/multiplayer-test-add-players": TestHandlers.multiplayerTestAddPlayers,
@@ -171,6 +171,8 @@ const routeMap: Record<string, Handler> = {
171
171
  "/api/find-and-replace-in-scripts": ScriptHandlers.findAndReplaceInScripts,
172
172
 
173
173
  "/api/get-runtime-logs": LogHandlers.getRuntimeLogs,
174
+ "/api/breakpoints": BreakpointHandlers.breakpoints,
175
+ "/api/capture-script-profiler": ScriptProfilerHandlers.captureScriptProfiler,
174
176
 
175
177
  "/api/export-rbxm": SerializationHandlers.exportRbxm,
176
178
  "/api/import-rbxm": SerializationHandlers.importRbxm,
@@ -57,21 +57,47 @@ function dropOldestUntilFits(incomingBytes: number): void {
57
57
  }
58
58
  }
59
59
 
60
+ function pushEntry(msg: string, t: Enum.MessageType, ts = nowSec()): void {
61
+ const bytes = msg.size();
62
+ dropOldestUntilFits(bytes);
63
+ entries.push({
64
+ seq: nextSeq,
65
+ ts,
66
+ level: levelTag(t),
67
+ message: msg,
68
+ });
69
+ nextSeq += 1;
70
+ totalBytes += bytes;
71
+ }
72
+
73
+ interface LogHistoryEntry {
74
+ message: string;
75
+ messageType: Enum.MessageType;
76
+ timestamp: number;
77
+ }
78
+
79
+ function seedRuntimeHistory(): void {
80
+ if (!RunService.IsRunning()) return;
81
+
82
+ const [ok, history] = pcall(() => LogService.GetLogHistory() as LogHistoryEntry[]);
83
+ if (!ok) return;
84
+
85
+ for (const entry of history) {
86
+ if (!typeIs(entry.message, "string")) continue;
87
+ pushEntry(entry.message, entry.messageType, typeIs(entry.timestamp, "number") ? entry.timestamp : undefined);
88
+ }
89
+ }
90
+
60
91
  function install(): void {
61
92
  if (installed) return;
62
93
  if (!RunService.IsStudio()) return;
63
94
  installed = true;
95
+ // Play peers can emit startup logs before the plugin finishes loading.
96
+ // Seed from per-DataModel LogHistory so get_runtime_logs can still see
97
+ // those early messages; skip edit mode to avoid stale prior-session logs.
98
+ seedRuntimeHistory();
64
99
  LogService.MessageOut.Connect((msg, t) => {
65
- const bytes = msg.size();
66
- dropOldestUntilFits(bytes);
67
- entries.push({
68
- seq: nextSeq,
69
- ts: nowSec(),
70
- level: levelTag(t),
71
- message: msg,
72
- });
73
- nextSeq += 1;
74
- totalBytes += bytes;
100
+ pushEntry(msg, t);
75
101
  });
76
102
  }
77
103
 
@@ -0,0 +1,460 @@
1
+ import Utils from "../Utils";
2
+
3
+ const { getInstanceByPath } = Utils;
4
+
5
+ const HttpService = game.GetService("HttpService");
6
+ const RunService = game.GetService("RunService");
7
+ const ServerStorage = game.GetService("ServerStorage");
8
+
9
+ const LOG_PREFIX = "Breakpoint";
10
+ const REGISTRY_KEY_PREFIX = "MCP_BREAKPOINTS_V1_";
11
+ const MCP_PLACE_ID_ATTRIBUTE = "__MCPPlaceId";
12
+
13
+ let pluginRef: Plugin | undefined;
14
+ let loadedRegistryKey: string | undefined;
15
+ let loadedRegistryFromSettings = false;
16
+
17
+ interface ScriptBreakpointSpec {
18
+ Line: number;
19
+ Enabled?: boolean;
20
+ Condition?: string;
21
+ LogMessage?: string;
22
+ ContinueExecution?: boolean;
23
+ }
24
+
25
+ interface ScriptBreakpointResult {
26
+ Verified?: boolean;
27
+ Line?: number;
28
+ Message?: string;
29
+ }
30
+
31
+ interface ScriptDebuggerServiceLike extends Instance {
32
+ AddBreakpoint(this: ScriptDebuggerServiceLike, script: Instance, breakpoint: ScriptBreakpointSpec): ScriptBreakpointResult;
33
+ RemoveBreakpoint(this: ScriptDebuggerServiceLike, script: Instance, line: number): boolean;
34
+ ClearBreakpoints(this: ScriptDebuggerServiceLike): void;
35
+ }
36
+
37
+ interface BreakpointEntry {
38
+ script_path: string;
39
+ line: number;
40
+ requested_line?: number;
41
+ enabled?: boolean;
42
+ condition?: string;
43
+ log_message?: string;
44
+ continue_execution?: boolean;
45
+ verified?: false;
46
+ message?: string;
47
+ created_at?: number;
48
+ }
49
+
50
+ interface PersistedBreakpointEntry {
51
+ script_path: string;
52
+ line: number;
53
+ }
54
+
55
+ interface RegistryScope {
56
+ key: string;
57
+ }
58
+
59
+ const breakpoints = new Map<string, BreakpointEntry>();
60
+
61
+ function init(p: Plugin): void {
62
+ pluginRef = p;
63
+ }
64
+
65
+ function breakpointKey(scriptPath: string, line: number): string {
66
+ return `${scriptPath}:${line}`;
67
+ }
68
+
69
+ function computeInstanceId(): string {
70
+ if (game.PlaceId !== 0) {
71
+ return `place:${tostring(game.PlaceId)}`;
72
+ }
73
+ const existing = ServerStorage.GetAttribute(MCP_PLACE_ID_ATTRIBUTE);
74
+ if (typeIs(existing, "string") && existing !== "") {
75
+ return `anon:${existing as string}`;
76
+ }
77
+ const fresh = HttpService.GenerateGUID(false);
78
+ pcall(() => ServerStorage.SetAttribute(MCP_PLACE_ID_ATTRIBUTE, fresh));
79
+ return `anon:${fresh}`;
80
+ }
81
+
82
+ function detectRole(): string {
83
+ if (!RunService.IsRunning()) return "edit";
84
+ if (RunService.IsServer()) return "server";
85
+ return "client";
86
+ }
87
+
88
+ function requestedRole(requestData: Record<string, unknown>): string {
89
+ return typeIs(requestData.__mcp_target_role, "string") && requestData.__mcp_target_role !== ""
90
+ ? requestData.__mcp_target_role as string
91
+ : detectRole();
92
+ }
93
+
94
+ function registryScope(requestData: Record<string, unknown>): RegistryScope {
95
+ const instanceId = typeIs(requestData.__mcp_instance_id, "string") && requestData.__mcp_instance_id !== ""
96
+ ? requestData.__mcp_instance_id as string
97
+ : computeInstanceId();
98
+ const role = requestedRole(requestData);
99
+ return {
100
+ key: `${REGISTRY_KEY_PREFIX}${instanceId}:${role}`,
101
+ };
102
+ }
103
+
104
+ function readSetting(key: string): unknown {
105
+ if (!pluginRef) return undefined;
106
+ const [ok, value] = pcall(() => pluginRef!.GetSetting(key));
107
+ return ok ? value : undefined;
108
+ }
109
+
110
+ function writeSetting(key: string, value: unknown): boolean {
111
+ if (!pluginRef) return false;
112
+ const [ok] = pcall(() => pluginRef!.SetSetting(key, value));
113
+ return ok;
114
+ }
115
+
116
+ function decodePersistedBreakpointEntry(value: unknown): BreakpointEntry | undefined {
117
+ if (!typeIs(value, "table")) return undefined;
118
+ const data = value as Record<string, unknown>;
119
+ if (!typeIs(data.script_path, "string") || data.script_path === "") return undefined;
120
+ if (!typeIs(data.line, "number") || data.line < 1) return undefined;
121
+
122
+ return {
123
+ script_path: data.script_path as string,
124
+ line: math.floor(data.line as number),
125
+ };
126
+ }
127
+
128
+ function loadRegistry(requestData: Record<string, unknown>): RegistryScope {
129
+ const scope = registryScope(requestData);
130
+ if (loadedRegistryKey !== scope.key) {
131
+ breakpoints.clear();
132
+ loadedRegistryKey = scope.key;
133
+ loadedRegistryFromSettings = false;
134
+ }
135
+
136
+ if (loadedRegistryFromSettings) return scope;
137
+ loadedRegistryFromSettings = true;
138
+
139
+ const stored = readSetting(scope.key);
140
+ if (stored === undefined) return scope;
141
+
142
+ let decoded: unknown = stored;
143
+ if (typeIs(stored, "string")) {
144
+ const [ok, result] = pcall(() => HttpService.JSONDecode(stored as string));
145
+ if (!ok) return scope;
146
+ decoded = result;
147
+ }
148
+ if (!typeIs(decoded, "table")) return scope;
149
+
150
+ breakpoints.clear();
151
+ for (const item of decoded as unknown[]) {
152
+ const entry = decodePersistedBreakpointEntry(item);
153
+ if (entry) {
154
+ breakpoints.set(breakpointKey(entry.script_path, entry.line), entry);
155
+ }
156
+ }
157
+ return scope;
158
+ }
159
+
160
+ interface PersistResult {
161
+ ok: boolean;
162
+ error?: string;
163
+ }
164
+
165
+ function persistRegistry(scope: RegistryScope): PersistResult {
166
+ if (!pluginRef) return { ok: false, error: "Plugin settings are unavailable; managed breakpoint registry is memory-only." };
167
+
168
+ const out: PersistedBreakpointEntry[] = [];
169
+ for (const [, entry] of breakpoints) {
170
+ out.push({
171
+ script_path: entry.script_path,
172
+ line: entry.line,
173
+ });
174
+ }
175
+
176
+ const [encodedOk, encoded] = pcall(() => HttpService.JSONEncode(out));
177
+ if (!encodedOk || !typeIs(encoded, "string")) {
178
+ return { ok: false, error: `Failed to encode managed breakpoint registry: ${tostring(encoded)}` };
179
+ }
180
+ if (!writeSetting(scope.key, encoded)) {
181
+ return { ok: false, error: "Failed to persist managed breakpoint registry with plugin:SetSetting." };
182
+ }
183
+ const stored = readSetting(scope.key);
184
+ if (stored !== encoded) {
185
+ return { ok: false, error: "Failed to verify managed breakpoint registry persistence after plugin:SetSetting." };
186
+ }
187
+ return { ok: true };
188
+ }
189
+
190
+ function attachPersistenceWarning(response: Record<string, unknown>, persist: PersistResult): Record<string, unknown> {
191
+ if (!persist.ok) {
192
+ response.managed_registry_persisted = false;
193
+ response.registry_error = persist.error;
194
+ }
195
+ return response;
196
+ }
197
+
198
+ function serviceError(message?: string): Record<string, unknown> {
199
+ return {
200
+ error: "script_debugger_unavailable",
201
+ message: message ?? "ScriptDebuggerService is unavailable. Enable the Studio Debugger Luau API beta feature and restart Studio.",
202
+ betaFeatureRequired: true,
203
+ };
204
+ }
205
+
206
+ function operationError(errorCode: string, operation: string, raw: unknown): Record<string, unknown> {
207
+ return {
208
+ error: errorCode,
209
+ message:
210
+ `${operation} failed. The breakpoints tool requires the Studio Debugger Luau API beta feature. ` +
211
+ "Enable it in Studio Beta Features and restart/reload Studio, then retry.",
212
+ rawMessage: tostring(raw),
213
+ betaFeatureRequired: true,
214
+ };
215
+ }
216
+
217
+ function getService(): ScriptDebuggerServiceLike | Record<string, unknown> {
218
+ const provider = game as unknown as { GetService(serviceName: string): Instance };
219
+ const [ok, service] = pcall(() => provider.GetService("ScriptDebuggerService") as ScriptDebuggerServiceLike);
220
+ if (!ok || !service) {
221
+ return serviceError(`ScriptDebuggerService unavailable: ${tostring(service)}`);
222
+ }
223
+ return service;
224
+ }
225
+
226
+ function luauStringLiteral(value: string): string {
227
+ let escaped = value.gsub("\\", "\\\\")[0];
228
+ escaped = escaped.gsub("\n", "\\n")[0];
229
+ escaped = escaped.gsub("\r", "\\r")[0];
230
+ escaped = escaped.gsub("\t", "\\t")[0];
231
+ escaped = escaped.gsub('"', '\\"')[0];
232
+ return `"${escaped}"`;
233
+ }
234
+
235
+ function buildLogMessage(scriptPath: string, line: number, logMessage: string | undefined): string {
236
+ const prefix = [
237
+ luauStringLiteral(LOG_PREFIX),
238
+ luauStringLiteral(`${scriptPath}:${line}`),
239
+ ];
240
+ if (typeIs(logMessage, "string") && logMessage !== "") {
241
+ prefix.push(logMessage);
242
+ }
243
+ return prefix.join(", ");
244
+ }
245
+
246
+ function listBreakpoints(requestData: Record<string, unknown>): Record<string, unknown> {
247
+ loadRegistry(requestData);
248
+ const out: BreakpointEntry[] = [];
249
+ for (const [, entry] of breakpoints) {
250
+ out.push(entry);
251
+ }
252
+ return {
253
+ breakpoints: out,
254
+ count: out.size(),
255
+ };
256
+ }
257
+
258
+ function setBreakpoint(requestData: Record<string, unknown>): unknown {
259
+ const scope = loadRegistry(requestData);
260
+ const serviceOrError = getService();
261
+ if (!serviceOrError.IsA) return serviceOrError;
262
+ const service = serviceOrError as ScriptDebuggerServiceLike;
263
+
264
+ const scriptPath = requestData.script_path as string | undefined;
265
+ const lineRaw = requestData.line as number | undefined;
266
+ if (!typeIs(scriptPath, "string") || scriptPath === "" || !typeIs(lineRaw, "number")) {
267
+ return { error: "invalid_args", message: "breakpoints action=set requires script_path and line" };
268
+ }
269
+
270
+ const requestedLine = math.floor(lineRaw);
271
+ if (requestedLine < 1) {
272
+ return { error: "invalid_line", message: "line must be a 1-based positive number" };
273
+ }
274
+
275
+ const instance = getInstanceByPath(scriptPath);
276
+ if (!instance) return { error: "script_not_found", script_path: scriptPath };
277
+ if (!instance.IsA("LuaSourceContainer")) {
278
+ return {
279
+ error: "not_a_script",
280
+ message: `${scriptPath} is ${instance.ClassName}, not a LuaSourceContainer`,
281
+ script_path: scriptPath,
282
+ };
283
+ }
284
+
285
+ const rawLogMessage = typeIs(requestData.log_message, "string") ? requestData.log_message as string : undefined;
286
+ const hasLogMessage = rawLogMessage !== undefined && rawLogMessage !== "";
287
+ const continueExecution = typeIs(requestData.continue_execution, "boolean")
288
+ ? requestData.continue_execution as boolean
289
+ : hasLogMessage;
290
+ const enabled = typeIs(requestData.enabled, "boolean") ? requestData.enabled as boolean : true;
291
+ const effectiveLogMessage = hasLogMessage || continueExecution ? buildLogMessage(scriptPath, requestedLine, rawLogMessage) : undefined;
292
+
293
+ const spec: ScriptBreakpointSpec = {
294
+ Line: requestedLine,
295
+ Enabled: enabled,
296
+ ContinueExecution: continueExecution,
297
+ };
298
+ if (typeIs(requestData.condition, "string") && requestData.condition !== "") {
299
+ spec.Condition = requestData.condition as string;
300
+ }
301
+ if (effectiveLogMessage !== undefined) {
302
+ spec.LogMessage = effectiveLogMessage;
303
+ }
304
+
305
+ const [ok, result] = pcall(() => service.AddBreakpoint(instance, spec));
306
+ if (!ok) return operationError("add_breakpoint_failed", "ScriptDebuggerService:AddBreakpoint", result);
307
+
308
+ const breakpointResult = result as ScriptBreakpointResult;
309
+ const actualLine = typeIs(breakpointResult.Line, "number") ? breakpointResult.Line : requestedLine;
310
+ const verified = typeIs(breakpointResult.Verified, "boolean") ? breakpointResult.Verified : undefined;
311
+ const message = typeIs(breakpointResult.Message, "string") ? breakpointResult.Message : undefined;
312
+ const entry: BreakpointEntry = {
313
+ script_path: scriptPath,
314
+ line: actualLine,
315
+ requested_line: actualLine !== requestedLine ? requestedLine : undefined,
316
+ enabled,
317
+ condition: spec.Condition,
318
+ log_message: rawLogMessage,
319
+ continue_execution: continueExecution,
320
+ verified: verified === false ? false : undefined,
321
+ message,
322
+ created_at: DateTime.now().UnixTimestampMillis,
323
+ };
324
+
325
+ breakpoints.set(breakpointKey(scriptPath, actualLine), entry);
326
+ return attachPersistenceWarning({
327
+ ok: true,
328
+ breakpoint: entry,
329
+ }, persistRegistry(scope));
330
+ }
331
+
332
+ function removeBreakpoint(requestData: Record<string, unknown>): unknown {
333
+ const scope = loadRegistry(requestData);
334
+ const serviceOrError = getService();
335
+ if (!serviceOrError.IsA) return serviceOrError;
336
+ const service = serviceOrError as ScriptDebuggerServiceLike;
337
+
338
+ const scriptPath = requestData.script_path as string | undefined;
339
+ const lineRaw = requestData.line as number | undefined;
340
+ if (!typeIs(scriptPath, "string") || scriptPath === "" || !typeIs(lineRaw, "number")) {
341
+ return { error: "invalid_args", message: "breakpoints action=remove requires script_path and line" };
342
+ }
343
+ const line = math.floor(lineRaw);
344
+ if (line < 1) {
345
+ return { error: "invalid_line", message: "line must be a 1-based positive number" };
346
+ }
347
+
348
+ const instance = getInstanceByPath(scriptPath);
349
+ if (!instance) return { error: "script_not_found", script_path: scriptPath };
350
+ if (!instance.IsA("LuaSourceContainer")) {
351
+ return {
352
+ error: "not_a_script",
353
+ message: `${scriptPath} is ${instance.ClassName}, not a LuaSourceContainer`,
354
+ script_path: scriptPath,
355
+ };
356
+ }
357
+
358
+ const [ok, removed] = pcall(() => service.RemoveBreakpoint(instance, line));
359
+ if (!ok) return operationError("remove_breakpoint_failed", "ScriptDebuggerService:RemoveBreakpoint", removed);
360
+
361
+ breakpoints.delete(breakpointKey(scriptPath, line));
362
+ return attachPersistenceWarning({
363
+ ok: true,
364
+ removed,
365
+ script_path: scriptPath,
366
+ line,
367
+ }, persistRegistry(scope));
368
+ }
369
+
370
+ function clearManagedBreakpoints(requestData: Record<string, unknown>): unknown {
371
+ const scope = loadRegistry(requestData);
372
+ const serviceOrError = getService();
373
+ if (!serviceOrError.IsA) return serviceOrError;
374
+ const service = serviceOrError as ScriptDebuggerServiceLike;
375
+
376
+ let cleared = 0;
377
+ const errors: Record<string, unknown>[] = [];
378
+
379
+ for (const [key, entry] of breakpoints) {
380
+ const instance = getInstanceByPath(entry.script_path);
381
+ if (!instance || !instance.IsA("LuaSourceContainer")) {
382
+ breakpoints.delete(key);
383
+ cleared += 1;
384
+ continue;
385
+ }
386
+
387
+ const [ok, removedOrError] = pcall(() => service.RemoveBreakpoint(instance, entry.line));
388
+ if (ok) {
389
+ breakpoints.delete(key);
390
+ cleared += 1;
391
+ } else {
392
+ errors.push({
393
+ script_path: entry.script_path,
394
+ line: entry.line,
395
+ error: tostring(removedOrError),
396
+ });
397
+ }
398
+ }
399
+
400
+ if (errors.size() > 0) {
401
+ return {
402
+ ok: false,
403
+ cleared,
404
+ errors,
405
+ };
406
+ }
407
+
408
+ return attachPersistenceWarning({
409
+ ok: true,
410
+ cleared,
411
+ }, persistRegistry(scope));
412
+ }
413
+
414
+ function clearAllBreakpoints(requestData: Record<string, unknown>): unknown {
415
+ const scope = loadRegistry(requestData);
416
+ const serviceOrError = getService();
417
+ if (!serviceOrError.IsA) return serviceOrError;
418
+ const service = serviceOrError as ScriptDebuggerServiceLike;
419
+
420
+ const managedCount = breakpoints.size();
421
+ const [ok, err] = pcall(() => service.ClearBreakpoints());
422
+ if (!ok) return operationError("clear_breakpoints_failed", "ScriptDebuggerService:ClearBreakpoints", err);
423
+ breakpoints.clear();
424
+ return attachPersistenceWarning({
425
+ ok: true,
426
+ cleared_managed: managedCount,
427
+ }, persistRegistry(scope));
428
+ }
429
+
430
+ function clearBreakpoints(requestData: Record<string, unknown>): unknown {
431
+ if (requestData.clear_all === true) {
432
+ return clearAllBreakpoints(requestData);
433
+ }
434
+ return clearManagedBreakpoints(requestData);
435
+ }
436
+
437
+ function breakpointsTool(requestData: Record<string, unknown>): unknown {
438
+ const action = requestData.action as string | undefined;
439
+ if (!typeIs(action, "string") || action === "") {
440
+ return { error: "invalid_args", message: "breakpoints requires action=set|remove|clear|list" };
441
+ }
442
+
443
+ switch (action) {
444
+ case "set":
445
+ return setBreakpoint(requestData);
446
+ case "remove":
447
+ return removeBreakpoint(requestData);
448
+ case "clear":
449
+ return clearBreakpoints(requestData);
450
+ case "list":
451
+ return listBreakpoints(requestData);
452
+ default:
453
+ return {
454
+ error: "unknown_action",
455
+ message: `breakpoints action must be one of: set, remove, clear, list (got ${action})`,
456
+ };
457
+ }
458
+ }
459
+
460
+ export = { breakpoints: breakpointsTool, init };
@@ -792,38 +792,6 @@ function compareInstances(requestData: Record<string, unknown>) {
792
792
  };
793
793
  }
794
794
 
795
- function getOutputLog(requestData: Record<string, unknown>) {
796
- const maxEntries = (requestData.maxEntries as number) ?? 100;
797
- const messageTypeFilter = requestData.messageType as string | undefined;
798
-
799
- const [success, result] = pcall(() => {
800
- const LogService = game.GetService("LogService");
801
- const history = LogService.GetLogHistory();
802
- const allEntries: Record<string, unknown>[] = [];
803
-
804
- for (const entry of history) {
805
- const msgType = tostring(entry.messageType);
806
- if (messageTypeFilter && msgType !== messageTypeFilter) continue;
807
- allEntries.push({
808
- message: entry.message,
809
- messageType: msgType,
810
- timestamp: entry.timestamp,
811
- });
812
- }
813
-
814
- const startIdx = math.max(0, allEntries.size() - maxEntries);
815
- const finalEntries: Record<string, unknown>[] = [];
816
- for (let i = startIdx; i < allEntries.size(); i++) {
817
- finalEntries.push(allEntries[i]);
818
- }
819
-
820
- return { entries: finalEntries, count: finalEntries.size(), totalAvailable: allEntries.size() };
821
- });
822
-
823
- if (success) return result;
824
- return { error: `Failed to get output log: ${result}` };
825
- }
826
-
827
795
  export = {
828
796
  getFileTree,
829
797
  searchFiles,
@@ -838,5 +806,4 @@ export = {
838
806
  grepScripts,
839
807
  getDescendants,
840
808
  compareInstances,
841
- getOutputLog,
842
809
  };