@chrrxs/robloxstudio-mcp 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.
package/dist/index.js CHANGED
@@ -891,13 +891,23 @@ var init_http_server = __esm({
891
891
  capture_device_matrix: (tools, body) => tools.captureDeviceMatrix(body.entries, body.target, body.format, body.quality, body.settleSeconds, body.restoreAfter, body.instance_id),
892
892
  start_playtest: (tools, body) => tools.startPlaytest(body.mode, body.numPlayers, body.instance_id),
893
893
  stop_playtest: (tools, body) => tools.stopPlaytest(body.instance_id),
894
- get_playtest_output: (tools, body) => tools.getPlaytestOutput(body.target, body.instance_id),
895
894
  multiplayer_test_start: (tools, body) => tools.multiplayerTestStart(body.numPlayers, body.testArgs, body.timeout, body.instance_id),
896
895
  multiplayer_test_state: (tools, body) => tools.multiplayerTestState(body.instance_id),
897
896
  multiplayer_test_add_players: (tools, body) => tools.multiplayerTestAddPlayers(body.numPlayers, body.timeout, body.instance_id),
898
897
  multiplayer_test_leave_client: (tools, body) => tools.multiplayerTestLeaveClient(body.target, body.timeout, body.instance_id),
899
898
  multiplayer_test_end: (tools, body) => tools.multiplayerTestEnd(body.value, body.timeout, body.instance_id),
900
899
  get_runtime_logs: (tools, body) => tools.getRuntimeLogs(body.target, body.since, body.tail, body.filter, body.instance_id),
900
+ capture_script_profiler: (tools, body) => tools.captureScriptProfiler(body.target, {
901
+ duration_ms: body.duration_ms,
902
+ frequency: body.frequency,
903
+ max_functions: body.max_functions,
904
+ min_total_us: body.min_total_us,
905
+ filter: body.filter,
906
+ include_native: body.include_native,
907
+ include_plugin: body.include_plugin,
908
+ output_path: body.output_path
909
+ }, body.instance_id),
910
+ breakpoints: (tools, body) => tools.breakpoints(body.action, body, body.target, body.instance_id),
901
911
  get_connected_instances: (tools) => tools.getConnectedInstances(),
902
912
  export_build: (tools, body) => tools.exportBuild(body.instancePath, body.outputId, body.style, body.instance_id),
903
913
  create_build: (tools, body) => tools.createBuild(body.id, body.style, body.palette, body.parts, body.bounds),
@@ -918,7 +928,6 @@ var init_http_server = __esm({
918
928
  clone_object: (tools, body) => tools.cloneObject(body.instancePath, body.targetParentPath, body.instance_id),
919
929
  get_descendants: (tools, body) => tools.getDescendants(body.instancePath, body.maxDepth, body.classFilter, body.instance_id),
920
930
  compare_instances: (tools, body) => tools.compareInstances(body.instancePathA, body.instancePathB, body.instance_id),
921
- get_output_log: (tools, body) => tools.getOutputLog(body.maxEntries, body.messageType, body.instance_id),
922
931
  bulk_set_attributes: (tools, body) => tools.bulkSetAttributes(body.instancePath, body.attributes, body.instance_id),
923
932
  capture_screenshot: (tools, body) => tools.captureScreenshot(body.instance_id, body.format, body.quality),
924
933
  simulate_mouse_input: (tools, body) => tools.simulateMouseInput(body.action, body.x, body.y, body.button, body.scrollDirection, body.target, body.instance_id),
@@ -4458,6 +4467,76 @@ ${code}`
4458
4467
  content: [{ type: "text", text: JSON.stringify(body) }]
4459
4468
  };
4460
4469
  }
4470
+ async captureScriptProfiler(target, request = {}, instance_id) {
4471
+ const targetRole = target ?? "server";
4472
+ const data = { ...request };
4473
+ const outputPath = data.output_path;
4474
+ delete data.output_path;
4475
+ if (outputPath !== void 0 && typeof outputPath !== "string") {
4476
+ throw new Error("output_path must be a string when provided");
4477
+ }
4478
+ if (outputPath) {
4479
+ data.__mcp_include_raw_json = true;
4480
+ }
4481
+ const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
4482
+ if (!resolved.ok)
4483
+ throw new RoutingFailure(resolved.error);
4484
+ if (resolved.mode !== "single") {
4485
+ throw new RoutingFailure({
4486
+ code: "target_role_not_present_on_instance",
4487
+ message: 'capture_script_profiler profiles one runtime peer at a time. Pick target="server" or a specific "client-N".',
4488
+ data: {
4489
+ instances: this.bridge.getPublicInstances(),
4490
+ count: this.bridge.getInstances().length
4491
+ }
4492
+ });
4493
+ }
4494
+ data.__mcp_instance_id = resolved.targetInstanceId;
4495
+ data.__mcp_target_role = resolved.targetRole;
4496
+ const response = await this.client.request("/api/capture-script-profiler", data, resolved.targetInstanceId, resolved.targetRole);
4497
+ const body = response !== null && typeof response === "object" && !Array.isArray(response) ? { ...response, target: resolved.targetRole } : response;
4498
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
4499
+ const mutable = body;
4500
+ const rawJson = mutable.raw_json;
4501
+ if (typeof rawJson === "string") {
4502
+ if (typeof outputPath === "string" && outputPath !== "") {
4503
+ const resolvedOutputPath = path.resolve(outputPath);
4504
+ fs.mkdirSync(path.dirname(resolvedOutputPath), { recursive: true });
4505
+ fs.writeFileSync(resolvedOutputPath, rawJson, "utf8");
4506
+ mutable.output_path = resolvedOutputPath;
4507
+ }
4508
+ delete mutable.raw_json;
4509
+ }
4510
+ }
4511
+ return { content: [{ type: "text", text: JSON.stringify(body) }] };
4512
+ }
4513
+ async breakpoints(action, request = {}, target, instance_id) {
4514
+ if (!action || typeof action !== "string") {
4515
+ throw new Error("breakpoints requires action=set|remove|clear|list");
4516
+ }
4517
+ const targetRole = target ?? "edit";
4518
+ const data = { ...request, action };
4519
+ delete data.target;
4520
+ delete data.instance_id;
4521
+ const resolved = this.bridge.resolveTarget({ instance_id, target: targetRole });
4522
+ if (!resolved.ok)
4523
+ throw new RoutingFailure(resolved.error);
4524
+ if (resolved.mode !== "single") {
4525
+ throw new RoutingFailure({
4526
+ code: "target_role_not_present_on_instance",
4527
+ message: "This tool does not support target=all. Pick a specific role or omit target.",
4528
+ data: {
4529
+ instances: this.bridge.getPublicInstances(),
4530
+ count: this.bridge.getInstances().length
4531
+ }
4532
+ });
4533
+ }
4534
+ data.__mcp_instance_id = resolved.targetInstanceId;
4535
+ data.__mcp_target_role = resolved.targetRole;
4536
+ const response = await this.client.request("/api/breakpoints", data, resolved.targetInstanceId, resolved.targetRole);
4537
+ const body = response !== null && typeof response === "object" && !Array.isArray(response) ? { ...response, target: resolved.targetRole } : response;
4538
+ return { content: [{ type: "text", text: JSON.stringify(body) }] };
4539
+ }
4461
4540
  async startPlaytest(mode, numPlayers, instance_id) {
4462
4541
  if (mode !== "play" && mode !== "run") {
4463
4542
  throw new Error('mode must be "play" or "run"');
@@ -4569,17 +4648,6 @@ ${code}`
4569
4648
  content: [{ type: "text", text: JSON.stringify(body) }]
4570
4649
  };
4571
4650
  }
4572
- async getPlaytestOutput(target, instance_id) {
4573
- const response = await this._callSingle("/api/get-playtest-output", {}, target || "edit", instance_id);
4574
- return {
4575
- content: [
4576
- {
4577
- type: "text",
4578
- text: JSON.stringify(response)
4579
- }
4580
- ]
4581
- };
4582
- }
4583
4651
  async _buildMultiplayerState(instanceId) {
4584
4652
  const peers = this.bridge.getPublicInstances().filter((i) => i.instanceId === instanceId).sort((a, b) => a.role.localeCompare(b.role));
4585
4653
  const body = {
@@ -5568,10 +5636,6 @@ ${code}`
5568
5636
  const response = await this._callSingle("/api/compare-instances", { instancePathA, instancePathB }, void 0, instance_id);
5569
5637
  return { content: [{ type: "text", text: JSON.stringify(response) }] };
5570
5638
  }
5571
- async getOutputLog(maxEntries, messageType, instance_id) {
5572
- const response = await this._callSingle("/api/get-output-log", { maxEntries, messageType }, void 0, instance_id);
5573
- return { content: [{ type: "text", text: JSON.stringify(response) }] };
5574
- }
5575
5639
  async bulkSetAttributes(instancePath, attributes, instance_id) {
5576
5640
  if (!instancePath || !attributes) {
5577
5641
  throw new Error("instancePath and attributes are required for bulk_set_attributes");
@@ -7100,7 +7164,7 @@ var init_definitions = __esm({
7100
7164
  {
7101
7165
  name: "start_playtest",
7102
7166
  category: "write",
7103
- description: "Start a simple single-player Studio playtest in play or run mode, waiting until a runtime peer registers with MCP. Captures print/warn/error via LogService. Poll with get_playtest_output, end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
7167
+ description: "Start a simple single-player Studio playtest in play or run mode, waiting until a runtime peer registers with MCP. Read print/warn/error output with get_runtime_logs, then end with stop_playtest. For multi-client testing use multiplayer_test_start instead.",
7104
7168
  inputSchema: {
7105
7169
  type: "object",
7106
7170
  properties: {
@@ -7124,28 +7188,10 @@ var init_definitions = __esm({
7124
7188
  {
7125
7189
  name: "stop_playtest",
7126
7190
  category: "write",
7127
- description: "Stop playtest and return all captured output.",
7128
- inputSchema: {
7129
- type: "object",
7130
- properties: {
7131
- instance_id: {
7132
- type: "string",
7133
- description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7134
- }
7135
- }
7136
- }
7137
- },
7138
- {
7139
- name: "get_playtest_output",
7140
- category: "read",
7141
- description: "Poll output buffer without stopping. Returns isRunning and captured messages.",
7191
+ description: "Stop playtest and wait for runtime peers to disconnect.",
7142
7192
  inputSchema: {
7143
7193
  type: "object",
7144
7194
  properties: {
7145
- target: {
7146
- type: "string",
7147
- description: 'Instance target: "edit" (default), "server", "client-1", "client-2", etc.'
7148
- },
7149
7195
  instance_id: {
7150
7196
  type: "string",
7151
7197
  description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
@@ -7536,7 +7582,7 @@ var init_definitions = __esm({
7536
7582
  {
7537
7583
  name: "get_runtime_logs",
7538
7584
  category: "read",
7539
- description: "Read the in-memory log buffers captured by Studio plugin peers. Each buffer captures ~64 KB of recent LogService.MessageOut entries; oldest entries drop when over budget. Entries include capturedBy for the plugin buffer that observed the log. In ordinary Studio play/run sessions, LogService reflects logs across edit/server/client, so script-origin peer is not reliable and entries omit peer. In StudioTestService multiplayer sessions only, peer attribution is reliable and entries also include peer. target=all (default) merges buffers and dedups same-message-and-level entries captured within 2s across different buffers.",
7585
+ description: "Read the in-memory log buffers captured by Studio plugin peers. Each buffer captures ~64 KB of recent LogService output; runtime peers seed from LogService:GetLogHistory() at plugin load so early startup logs emitted before the plugin finishes loading can still be returned, then continue capturing LogService.MessageOut entries. Oldest entries drop when over budget. Entries include capturedBy for the plugin buffer that observed the log. In ordinary Studio play/run sessions, LogService reflects logs across edit/server/client, so script-origin peer is not reliable and entries omit peer. In StudioTestService multiplayer sessions only, peer attribution is reliable and entries also include peer. target=all (default) merges buffers and dedups same-message-and-level entries captured within 2s across different buffers.",
7540
7586
  inputSchema: {
7541
7587
  type: "object",
7542
7588
  properties: {
@@ -7563,6 +7609,120 @@ var init_definitions = __esm({
7563
7609
  }
7564
7610
  }
7565
7611
  },
7612
+ {
7613
+ name: "capture_script_profiler",
7614
+ category: "read",
7615
+ description: `Capture one short ScriptProfilerService sample on a running server or client peer and return a compact CPU summary. Use this for Luau/script optimization, not render, physics, networking, or engine microprofiler lanes. Minimal flow: start or reproduce the workload, call capture_script_profiler with target="server" or a specific "client-N", inspect top_functions, patch the suspected hot path, then capture again with the same target/workload/duration_ms/frequency/filter/min_total_us to compare. top_functions is sorted by descending total_us after native/plugin/min/filter exclusions; each row includes rank plus function_index, the 1-based index into the raw Roblox Functions array. Function and node TotalDuration values follow Roblox's exported Script Profiler JSON format and are reported in microseconds as total_us. total_us is cumulative profiler TotalDuration during the capture; nested labels/functions can overlap, so do not sum rows as total CPU time. source is the runtime script path reported by Roblox and may need mapping back to editable source with search tools. If function names are too broad, add debug.profilebegin("Area:SpecificStep") / debug.profileend() around suspected code and pass filter="Area:" or another label prefix; matching custom labels appear in debug_labels and top_functions with their script source and no line number. The result echoes effective options in applied and omitted.filtered_out counts rows removed by filter. Keep captures short while actively triggering the behavior; duration_ms defaults to 1000 and is clamped to 100-15000. Pass output_path when you need the raw Roblox Script Profiler JSON for offline comparison or deeper analysis. This tool owns the start/stop/request profiler lifecycle for one capture and does not expose long-lived profiler sessions.`,
7616
+ inputSchema: {
7617
+ type: "object",
7618
+ properties: {
7619
+ target: {
7620
+ type: "string",
7621
+ pattern: "^(server|client-[0-9]+)$",
7622
+ description: 'Runtime peer to profile: "server" (default) or "client-N". Use get_connected_instances to discover available runtime roles. target="edit" is invalid because ScriptProfiler captures running code.'
7623
+ },
7624
+ duration_ms: {
7625
+ type: "number",
7626
+ default: 1e3,
7627
+ minimum: 100,
7628
+ maximum: 15e3,
7629
+ description: "Sample duration in milliseconds. Defaults to 1000; clamped to 100-15000 so the Studio bridge does not hang on long captures."
7630
+ },
7631
+ frequency: {
7632
+ type: "number",
7633
+ default: 1e3,
7634
+ minimum: 1,
7635
+ maximum: 1e4,
7636
+ description: "ScriptProfiler sampling frequency in samples per second (Hz). Defaults to 1000."
7637
+ },
7638
+ max_functions: {
7639
+ type: "number",
7640
+ default: 20,
7641
+ minimum: 1,
7642
+ maximum: 100,
7643
+ description: "Maximum number of top_functions and debug_labels to return. Defaults to 20; clamped to 1-100."
7644
+ },
7645
+ min_total_us: {
7646
+ type: "number",
7647
+ default: 0,
7648
+ minimum: 0,
7649
+ description: "Omit functions below this TotalDuration in microseconds after capture. Defaults to 0."
7650
+ },
7651
+ filter: {
7652
+ type: "string",
7653
+ description: "Optional case-insensitive substring matched against function name and source before top_functions are returned. Useful for focusing on one module or debug.profilebegin label prefix."
7654
+ },
7655
+ include_native: {
7656
+ type: "boolean",
7657
+ description: "Include native Roblox frames in top_functions. Defaults to false to keep optimization output focused on game Luau and debug labels."
7658
+ },
7659
+ include_plugin: {
7660
+ type: "boolean",
7661
+ description: "Include plugin frames in top_functions. Defaults to false because the MCP capture implementation can otherwise add noise."
7662
+ },
7663
+ output_path: {
7664
+ type: "string",
7665
+ description: "Optional local path where the MCP server writes the raw Script Profiler JSON. The tool result then includes output_path instead of inlining the raw JSON."
7666
+ },
7667
+ instance_id: {
7668
+ type: "string",
7669
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7670
+ }
7671
+ }
7672
+ }
7673
+ },
7674
+ {
7675
+ name: "breakpoints",
7676
+ category: "write",
7677
+ description: 'Manage Studio debugger breakpoints through ScriptDebuggerService. Use this when the user asks to debug with Studio breakpoints. Prefer log breakpoints for agent debugging: pass log_message and let continue_execution default to true, reproduce the issue, then read get_runtime_logs filtered by "Breakpoint". Minimal flow: set a log breakpoint, run or trigger the behavior, call get_runtime_logs with filter="Breakpoint", then call action="clear" to remove MCP-managed breakpoints. Generated breakpoint logs are prefixed with "Breakpoint" plus script_path:line; Studio breakpoint errors also start with "Breakpoint", so this filter captures both successful breakpoint logs and breakpoint-related failures. Set breakpoints on target="edit" before starting a playtest when possible; for an already-running playtest target the runtime DataModel directly, such as "server" or "client-1". Do not set continue_execution=false unless the target DataModel already has a ScriptDebuggerService.OnStopped handler that returns Enum.DebuggerResumeType.Resume for breakpoint/non-exception stops; otherwise the playtest can get stuck and MCP can lose the server/client peers. Minimal OnStopped reference: local sds=game:GetService("ScriptDebuggerService"); sds.OnStopped=function(info) if info.Reason ~= Enum.ScriptStoppedReason.Exception then return Enum.DebuggerResumeType.Resume end print("EXCEPTION:", info.ExceptionText); return Enum.DebuggerResumeType.Resume end. MCP-managed breakpoints persist minimal script_path/line recovery data per place and target so action="list" and action="clear" can find tool-created edit/server/client breakpoints after MCP/plugin reloads. action="clear" removes only breakpoints created through this MCP tool by default; pass clear_all=true only when you intentionally want to clear every Studio breakpoint in the targeted DataModel, including user-created breakpoints. This tool only manages breakpoint lifecycle; it does not pause, resume, step, inspect variables, or install OnStopped callbacks. Requires Studio Debugger Luau API beta enabled.',
7678
+ inputSchema: {
7679
+ type: "object",
7680
+ properties: {
7681
+ action: {
7682
+ type: "string",
7683
+ enum: ["set", "remove", "clear", "list"],
7684
+ description: "Breakpoint action to run. set/remove require script_path and line. clear removes MCP-managed breakpoints by default. list returns breakpoints created through this MCP tool in the targeted DataModel."
7685
+ },
7686
+ clear_all: {
7687
+ type: "boolean",
7688
+ description: 'Only applies to action="clear". Omit or set false to remove only MCP-managed breakpoints tracked by this tool. Set true to call ScriptDebuggerService:ClearBreakpoints() and clear every Studio breakpoint in the targeted DataModel, including user-created breakpoints.'
7689
+ },
7690
+ script_path: {
7691
+ type: "string",
7692
+ description: "Path to a LuaSourceContainer, for example game.ServerScriptService.Main. Required for set/remove."
7693
+ },
7694
+ line: {
7695
+ type: "number",
7696
+ description: "1-based line number for set/remove."
7697
+ },
7698
+ enabled: {
7699
+ type: "boolean",
7700
+ description: "Whether the breakpoint is enabled when set. Defaults to true."
7701
+ },
7702
+ condition: {
7703
+ type: "string",
7704
+ description: "Optional Luau condition expression for set."
7705
+ },
7706
+ log_message: {
7707
+ type: "string",
7708
+ description: `Optional Studio breakpoint log expression list for set, such as "'health', health". Literal text must be quoted as a Luau string. The tool prefixes this with "Breakpoint" and script_path:line. After reproducing, read get_runtime_logs with filter="Breakpoint" so breakpoint logs and Studio breakpoint errors are both visible.`
7709
+ },
7710
+ continue_execution: {
7711
+ type: "boolean",
7712
+ description: "Whether the breakpoint should log and continue without pausing. Defaults to true when log_message is provided; otherwise false. Only set false when you have first installed a ScriptDebuggerService.OnStopped handler on the same target that resumes breakpoint/non-exception stops with Enum.DebuggerResumeType.Resume; without that handler the playtest can get stuck and MCP can lose server/client peers."
7713
+ },
7714
+ target: {
7715
+ type: "string",
7716
+ description: 'Peer to target: "edit" (default), "server", or "client-N". Set edit breakpoints before playtests; target server/client-N for running play DataModels.'
7717
+ },
7718
+ instance_id: {
7719
+ type: "string",
7720
+ description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
7721
+ }
7722
+ },
7723
+ required: ["action"]
7724
+ }
7725
+ },
7566
7726
  // === Multi-Instance ===
7567
7727
  {
7568
7728
  name: "get_connected_instances",
@@ -8281,29 +8441,6 @@ part(0,2,0,2,1,1,"b")`,
8281
8441
  required: ["instancePathA", "instancePathB"]
8282
8442
  }
8283
8443
  },
8284
- // === Output & Diagnostics ===
8285
- {
8286
- name: "get_output_log",
8287
- category: "read",
8288
- description: "Get the Studio output log history. Works in both edit and play mode.",
8289
- inputSchema: {
8290
- type: "object",
8291
- properties: {
8292
- maxEntries: {
8293
- type: "number",
8294
- description: "Maximum number of log entries to return (default: 100)"
8295
- },
8296
- messageType: {
8297
- type: "string",
8298
- description: 'Filter by message type (e.g. "Enum.MessageType.MessageOutput", "Enum.MessageType.MessageWarning", "Enum.MessageType.MessageError")'
8299
- },
8300
- instance_id: {
8301
- type: "string",
8302
- description: "Which connected Studio place to target. Required when multiple places are connected; omit when one. Use get_connected_instances to list available IDs."
8303
- }
8304
- }
8305
- }
8306
- },
8307
8444
  // === Bulk Attributes ===
8308
8445
  {
8309
8446
  name: "bulk_set_attributes",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrrxs/robloxstudio-mcp",
3
- "version": "2.16.4",
3
+ "version": "2.17.0",
4
4
  "description": "MCP server for testing, debugging, and controlling Roblox Studio from AI coding tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",