conduit-mcp 2.0.4 → 2.1.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.
@@ -698,6 +698,10 @@ function truncateToTokenBudget(text, budget) {
698
698
 
699
699
  // src/utils/formatting.ts
700
700
  function formatTree(data, depth = 0, indent = "") {
701
+ if (data.className === "_collapsed") {
702
+ return `${indent} *${data.name}*
703
+ `;
704
+ }
701
705
  let line = `${indent}- **${data.name}** \`${data.className}\``;
702
706
  if (data.properties && Object.keys(data.properties).length > 0) {
703
707
  const props = Object.entries(data.properties).map(([k, v]) => `${k}=${formatValue(v)}`).join(", ");
@@ -844,10 +848,13 @@ function register(server, bridge) {
844
848
  "get_info",
845
849
  {
846
850
  title: "Get Instance Info",
847
- description: "Get detailed information about a single instance: class, parent, children count, properties, attributes, tags, and optionally a typed property list \u2014 all in one call.",
851
+ description: "Get detailed information about a single instance: class, parent, children count, properties, attributes, tags, and optionally a typed property list \u2014 all in one call.\n\nUse this to inspect runtime UI layout (Size, Position, Transparency, Visible, Text, etc.), part properties, lighting, sounds, and more. Reads ~60 common properties automatically. Use `propertyNames` to request only specific properties for a leaner response.\n\nFor UI debugging: check BackgroundTransparency, Visible, Size, Position, AnchorPoint, ZIndex, LayoutOrder on GuiObjects.",
848
852
  inputSchema: z.object({
849
853
  path: z.string().describe("Path to the instance, e.g. 'game.Workspace.Part'"),
850
- includeProperties: z.boolean().default(true).describe("Include property values"),
854
+ includeProperties: z.boolean().default(true).describe("Include property values (reads ~60 common properties)"),
855
+ propertyNames: z.array(z.string()).optional().describe(
856
+ "Request only these specific properties by name, e.g. ['Size', 'Position', 'Transparency']. When set, only these properties are returned (much leaner than the full property dump)."
857
+ ),
851
858
  includeAttributes: z.boolean().default(true).describe("Include custom attributes"),
852
859
  includeTags: z.boolean().default(true).describe("Include CollectionService tags"),
853
860
  includePropertyList: z.boolean().default(false).describe("Include a typed list of all discoverable properties with types and values"),
@@ -865,6 +872,7 @@ function register(server, bridge) {
865
872
  const result = await bridge.send("get_info", {
866
873
  path: params.path,
867
874
  includeProperties: params.includeProperties,
875
+ propertyNames: params.propertyNames,
868
876
  includeAttributes: params.includeAttributes,
869
877
  includeTags: params.includeTags,
870
878
  includePropertyList: params.includePropertyList,
@@ -1187,13 +1195,26 @@ function registerReadScript(server, bridge) {
1187
1195
  "read_script",
1188
1196
  {
1189
1197
  title: "Read Script Source",
1190
- description: "Read the Lua source code of a script instance. Optionally restrict to a line range.",
1198
+ description: "Read the Lua source code of a script instance. Optionally restrict to a line range. Use outline=true to get just function signatures and top-level declarations with line numbers \u2014 much cheaper than reading the full source for large scripts.\n\nSupports batch mode: pass `paths` (array) instead of `path` to read multiple scripts in a single call. Each entry can independently use outline or lineRange.",
1191
1199
  inputSchema: z3.object({
1192
- path: z3.string().describe("Path to the script instance"),
1200
+ path: z3.string().optional().describe("Path to a single script instance"),
1201
+ paths: z3.array(
1202
+ z3.object({
1203
+ path: z3.string().describe("Script path"),
1204
+ outline: z3.boolean().default(false).describe("Return outline instead of source"),
1205
+ lineRange: z3.object({
1206
+ start: z3.number().int().min(1).describe("Start line (1-based)"),
1207
+ end: z3.number().int().min(1).describe("End line (1-based, inclusive)")
1208
+ }).optional().describe("Optional line range")
1209
+ })
1210
+ ).optional().describe("Batch mode: read multiple scripts in one call. Each entry can independently use outline or lineRange."),
1211
+ outline: z3.boolean().default(false).describe(
1212
+ "If true, return only a structural outline (function signatures, top-level locals, types, return) with line numbers instead of full source. Ideal for navigating large scripts."
1213
+ ),
1193
1214
  lineRange: z3.object({
1194
1215
  start: z3.number().int().min(1).describe("Start line (1-based)"),
1195
1216
  end: z3.number().int().min(1).describe("End line (1-based, inclusive)")
1196
- }).optional().describe("Optional line range to read"),
1217
+ }).optional().describe("Optional line range to read (ignored when outline=true)"),
1197
1218
  maxTokens: z3.number().optional().describe("Maximum token budget for the response")
1198
1219
  }),
1199
1220
  annotations: {
@@ -1204,6 +1225,69 @@ function registerReadScript(server, bridge) {
1204
1225
  }
1205
1226
  },
1206
1227
  async (params) => {
1228
+ if (params.paths && params.paths.length > 0) {
1229
+ const result2 = await bridge.send("batch_read_scripts", {
1230
+ scripts: params.paths
1231
+ });
1232
+ const sections = [];
1233
+ for (const r of result2.results) {
1234
+ if (r.outline) {
1235
+ const lines = [`### Outline: \`${r.path}\` (${r.totalLines} lines)`, ""];
1236
+ for (const entry of r.outline) {
1237
+ const pad = " ".repeat(entry.indent);
1238
+ lines.push(`${entry.line}: ${pad}${entry.text}`);
1239
+ }
1240
+ if (r.outline.length === 0) {
1241
+ lines.push("*No functions, type definitions, or top-level declarations found.*");
1242
+ }
1243
+ sections.push(lines.join("\n"));
1244
+ } else {
1245
+ sections.push(formatScript(r.source ?? "", r.path));
1246
+ }
1247
+ }
1248
+ if (result2.errors && result2.errors.length > 0) {
1249
+ const errLines = ["**Errors:**"];
1250
+ for (const e of result2.errors) {
1251
+ errLines.push(`- \`${e.path}\`: ${e.error}`);
1252
+ }
1253
+ sections.push(errLines.join("\n"));
1254
+ }
1255
+ const text2 = sections.join("\n\n---\n\n");
1256
+ return {
1257
+ content: [
1258
+ { type: "text", text: applyTokenBudget(text2, params.maxTokens) }
1259
+ ]
1260
+ };
1261
+ }
1262
+ if (!params.paths && !params.path) {
1263
+ return {
1264
+ content: [{ type: "text", text: "Provide either `path` (single script) or `paths` (batch read)." }],
1265
+ isError: true
1266
+ };
1267
+ }
1268
+ const path = params.path;
1269
+ if (params.outline) {
1270
+ const result2 = await bridge.send("outline_script", {
1271
+ path
1272
+ });
1273
+ const lines = [
1274
+ `### Outline: \`${result2.path}\` (${result2.totalLines} lines)`,
1275
+ ""
1276
+ ];
1277
+ for (const entry of result2.outline) {
1278
+ const pad = " ".repeat(entry.indent);
1279
+ lines.push(`${entry.line}: ${pad}${entry.text}`);
1280
+ }
1281
+ if (result2.outline.length === 0) {
1282
+ lines.push("*No functions, type definitions, or top-level declarations found.*");
1283
+ }
1284
+ const text2 = lines.join("\n");
1285
+ return {
1286
+ content: [
1287
+ { type: "text", text: applyTokenBudget(text2, params.maxTokens) }
1288
+ ]
1289
+ };
1290
+ }
1207
1291
  if (params.lineRange && params.lineRange.end < params.lineRange.start) {
1208
1292
  return {
1209
1293
  content: [{ type: "text", text: "lineRange.end must be >= lineRange.start." }],
@@ -1211,10 +1295,10 @@ function registerReadScript(server, bridge) {
1211
1295
  };
1212
1296
  }
1213
1297
  const result = await bridge.send("read_script", {
1214
- path: params.path,
1298
+ path,
1215
1299
  lineRange: params.lineRange
1216
1300
  });
1217
- const text = formatScript(result.source, params.path);
1301
+ const text = formatScript(result.source, path);
1218
1302
  return {
1219
1303
  content: [
1220
1304
  { type: "text", text: applyTokenBudget(text, params.maxTokens) }
@@ -1228,10 +1312,10 @@ function registerWriteTools(server, bridge) {
1228
1312
  "edit_script",
1229
1313
  {
1230
1314
  title: "Edit Script Source",
1231
- description: "Edit the Lua source code of a script. Supports four modes: 'full' replaces the entire source, 'range' replaces specific line/column ranges, 'find_replace' does text find-and-replace on one script, and 'multi_replace' does find-and-replace across multiple scripts in one undoable operation.",
1315
+ description: "Edit the Lua source code of a script. Supports five modes:\n- 'full': Replace entire source.\n- 'range': Replace specific line/column ranges.\n- 'find_replace': Text find-and-replace on one script.\n- 'multi_replace': Same find-and-replace across multiple scripts.\n- 'batch': Different find-and-replace edits across different scripts in one atomic operation. Use this when you need different changes in different scripts.\n\nTip: Wrap multiple edits in a transaction (begin/commit) to group them into a single Ctrl+Z undo point.",
1232
1316
  inputSchema: z3.object({
1233
- path: z3.string().describe("Path to the script instance"),
1234
- mode: z3.enum(["full", "range", "find_replace", "multi_replace"]).describe("Edit mode: full, range, find_replace, or multi_replace"),
1317
+ path: z3.string().optional().describe("Path to the script instance (not needed for 'multi_replace' or 'batch' modes)"),
1318
+ mode: z3.enum(["full", "range", "find_replace", "multi_replace", "batch"]).describe("Edit mode"),
1235
1319
  source: z3.string().optional().describe("Complete new source (for 'full' mode)"),
1236
1320
  edits: z3.array(
1237
1321
  z3.object({
@@ -1245,7 +1329,18 @@ function registerWriteTools(server, bridge) {
1245
1329
  find: z3.string().optional().describe("Text or pattern to find (for 'find_replace' mode)"),
1246
1330
  replace: z3.string().optional().describe("Replacement text (for 'find_replace' mode)"),
1247
1331
  regex: z3.boolean().optional().describe("Treat 'find' as a Lua pattern (for 'find_replace' and 'multi_replace' modes)"),
1248
- scripts: z3.array(z3.string()).optional().describe("Array of script paths to apply find/replace across (for 'multi_replace' mode)")
1332
+ scripts: z3.array(z3.string()).optional().describe("Array of script paths to apply find/replace across (for 'multi_replace' mode)"),
1333
+ batch: z3.array(
1334
+ z3.object({
1335
+ path: z3.string().describe("Script path"),
1336
+ source: z3.string().optional().describe("Complete new source (for full replacement). Mutually exclusive with find/replace."),
1337
+ find: z3.string().optional().describe("Text or pattern to find"),
1338
+ replace: z3.string().default("").describe("Replacement text (used with find)"),
1339
+ regex: z3.boolean().default(false).describe("Treat find as a Lua pattern (used with find)")
1340
+ })
1341
+ ).optional().describe(
1342
+ "Array of per-script edit operations (for 'batch' mode). Each entry can either use `source` for full replacement or `find`/`replace` for targeted edits."
1343
+ )
1249
1344
  }),
1250
1345
  annotations: {
1251
1346
  readOnlyHint: false,
@@ -1307,6 +1402,51 @@ function registerWriteTools(server, bridge) {
1307
1402
  }
1308
1403
  return { content: [{ type: "text", text: lines.join("\n") }] };
1309
1404
  }
1405
+ if (params.mode === "batch") {
1406
+ if (!params.batch || params.batch.length === 0) {
1407
+ return {
1408
+ content: [{ type: "text", text: "batch mode requires a non-empty `batch` array." }],
1409
+ isError: true
1410
+ };
1411
+ }
1412
+ for (const entry of params.batch) {
1413
+ if (entry.source !== void 0 && entry.find !== void 0) {
1414
+ return {
1415
+ content: [{ type: "text", text: `Batch entry for \`${entry.path}\`: provide either \`source\` or \`find\`, not both.` }],
1416
+ isError: true
1417
+ };
1418
+ }
1419
+ if (entry.source === void 0 && entry.find === void 0) {
1420
+ return {
1421
+ content: [{ type: "text", text: `Batch entry for \`${entry.path}\`: must provide either \`source\` (full replacement) or \`find\` (find/replace).` }],
1422
+ isError: true
1423
+ };
1424
+ }
1425
+ }
1426
+ const result2 = await bridge.send("batch_edit_scripts", {
1427
+ edits: params.batch
1428
+ });
1429
+ const lines = [
1430
+ `**Batch edit** \u2014 ${result2.scriptsModified} script(s) modified`,
1431
+ ""
1432
+ ];
1433
+ for (const r of result2.results) {
1434
+ if (r.success) {
1435
+ if (r.mode === "full") {
1436
+ lines.push(`- \`${r.path}\`: full source replaced`);
1437
+ } else {
1438
+ lines.push(`- \`${r.path}\`: ${r.replacements ?? 0} replacement(s)`);
1439
+ }
1440
+ }
1441
+ }
1442
+ if (result2.errors && result2.errors.length > 0) {
1443
+ lines.push("", "**Errors:**");
1444
+ for (const e of result2.errors) {
1445
+ lines.push(`- \`${e.path}\`: ${e.error}`);
1446
+ }
1447
+ }
1448
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1449
+ }
1310
1450
  const result = await bridge.send("edit_script", {
1311
1451
  path: params.path,
1312
1452
  mode: params.mode,
@@ -1379,9 +1519,9 @@ function register4(server, bridge) {
1379
1519
  "playtest",
1380
1520
  {
1381
1521
  title: "Playtest Control & Virtual Input",
1382
- description: "Control Roblox Studio playtesting and simulate user input.\n\nActions:\n- `start`: Begin a playtest session. Defaults to Play mode (F5, full client with player character). Set mode='run' for Run mode (F8, server-only, no player).\n- `stop`: End the current playtest.\n- `execute`: Run Lua code in the running game context.\n- `get_output`: Get console/log output from Studio (works in edit mode and during playtest).\n- `inspect`: Evaluate a Luau expression and return the typed result (requires active playtest).\n- `navigate`: Walk the player character to a position using PathfindingService (requires client playtest).\n- `mouse_click`: Simulate a mouse click at screen coordinates.\n- `mouse_move`: Move the virtual mouse to screen coordinates.\n- `key_press`: Press and release a key.\n- `key_down`: Hold a key down.\n- `key_up`: Release a held key.",
1522
+ description: "Control Roblox Studio playtesting and simulate user input.\n\nActions:\n- `start`: Begin a playtest session. Defaults to Play mode (F5, full client with player character). Set mode='run' for Run mode (F8, server-only, no player).\n- `stop`: End the current playtest.\n- `execute`: Run Lua code in the running game context.\n- `get_output`: Get console/log output from Studio (works in edit mode and during playtest).\n- `inspect`: Evaluate a Luau expression and return the typed result (requires active playtest).\n- `navigate`: Walk the player character to a position using PathfindingService (requires client playtest).\n- `mouse_click`: Simulate a mouse click at screen coordinates.\n- `mouse_move`: Move the virtual mouse to screen coordinates.\n- `key_press`: Press and release a key.\n- `key_down`: Hold a key down.\n- `key_up`: Release a held key.\n- `screenshot`: Capture the viewport during playtest. Useful for seeing the game state visually.",
1383
1523
  inputSchema: z4.object({
1384
- action: z4.enum(["start", "stop", "execute", "get_output", "inspect", "navigate", "mouse_click", "mouse_move", "key_press", "key_down", "key_up"]).describe("Playtest action"),
1524
+ action: z4.enum(["start", "stop", "execute", "get_output", "inspect", "navigate", "mouse_click", "mouse_move", "key_press", "key_down", "key_up", "screenshot"]).describe("Playtest action"),
1385
1525
  mode: z4.enum(["play", "run"]).default("play").describe("Playtest mode: 'play' (F5, full client with player) or 'run' (F8, server-only). Default: play"),
1386
1526
  code: z4.string().optional().describe("Lua code to execute (for 'execute' action)"),
1387
1527
  // get_output params
@@ -1465,6 +1605,25 @@ ${logText}
1465
1605
  ${result2.message}` : ""}`;
1466
1606
  return { content: [{ type: "text", text: text2 }] };
1467
1607
  }
1608
+ if (params.action === "screenshot") {
1609
+ const result2 = await bridge.send("screenshot", {});
1610
+ if (result2.imageBase64 && result2.mimeType) {
1611
+ return {
1612
+ content: [
1613
+ {
1614
+ type: "image",
1615
+ data: result2.imageBase64,
1616
+ mimeType: result2.mimeType
1617
+ }
1618
+ ]
1619
+ };
1620
+ }
1621
+ return {
1622
+ content: [
1623
+ { type: "text", text: `Playtest screenshot: ${result2.message ?? result2.status}` }
1624
+ ]
1625
+ };
1626
+ }
1468
1627
  if (params.action === "mouse_click" || params.action === "mouse_move" || params.action === "key_press" || params.action === "key_down" || params.action === "key_up") {
1469
1628
  if ((params.action === "mouse_click" || params.action === "mouse_move") && (params.x === void 0 || params.y === void 0)) {
1470
1629
  return {
@@ -1955,7 +2114,7 @@ function register7(server, bridge) {
1955
2114
  "screenshot",
1956
2115
  {
1957
2116
  title: "Take Screenshot",
1958
- description: "Capture a screenshot of the current Roblox Studio viewport. Returns base64 image data when available for vision model consumption.",
2117
+ description: "Capture a screenshot of the current Roblox Studio viewport. Works in both edit mode and during playtest.\n\nThe screenshot is saved to the user's Roblox screenshots folder. Base64 image data is returned when the EditableImage API is available (Roblox platform limitation).\n\nTip: During playtest, use this to capture the game viewport. Combine with `playtest start` + a short delay via `playtest execute` (e.g. `task.wait(2)`) to capture after the game loads.",
1959
2118
  inputSchema: z7.object({}),
1960
2119
  annotations: {
1961
2120
  readOnlyHint: true,
@@ -2014,10 +2173,11 @@ function register7(server, bridge) {
2014
2173
  "transaction",
2015
2174
  {
2016
2175
  title: "Transaction Control",
2017
- description: "Group multiple mutating tool calls into a single Ctrl+Z undo point.\n\nUsage: ALWAYS call `begin` first, then make your changes, then call `commit` or `rollback`. Calling `commit` or `rollback` without a prior `begin` will error.\n\nActions:\n- `begin`: Start a transaction. All subsequent writes share one undo recording.\n- `commit`: Finish an open transaction and commit all changes as one undo point. Requires a prior `begin`.\n- `rollback`: Cancel an open transaction and undo all changes made since begin. Requires a prior `begin`.\n\nTransactions auto-rollback after 60 seconds if not committed.",
2176
+ description: "Group multiple tool calls into a single Ctrl+Z undo point \u2014 essential when making several related edits that the user should be able to revert together.\n\n**When to use:** Before starting a multi-edit session (e.g. refactoring across scripts, creating multiple instances, UI changes). Call `begin`, make all your changes, then `commit`. The user can undo everything in one Ctrl+Z.\n\nActions:\n- `begin`: Start a transaction. All subsequent writes share one undo recording.\n- `commit`: Finish and commit all changes as one undo point.\n- `rollback`: Cancel and revert all changes since begin.\n\nTransactions auto-rollback after the timeout (default 120s) if not committed.",
2018
2177
  inputSchema: z7.object({
2019
2178
  action: z7.enum(["begin", "commit", "rollback"]).describe("Transaction action"),
2020
- name: z7.string().optional().describe("Transaction name for the undo history (for 'begin' action)")
2179
+ name: z7.string().optional().describe("Transaction name for the undo history (for 'begin' action)"),
2180
+ timeout: z7.number().int().min(10).max(300).optional().describe("Auto-rollback timeout in seconds (default 120, min 10, max 300). Increase for large multi-edit sessions.")
2021
2181
  }),
2022
2182
  annotations: {
2023
2183
  readOnlyHint: false,
@@ -2031,7 +2191,8 @@ function register7(server, bridge) {
2031
2191
  if (params.action === "begin") {
2032
2192
  try {
2033
2193
  const result = await bridge.send("begin_transaction", {
2034
- name: params.name
2194
+ name: params.name,
2195
+ timeout: params.timeout
2035
2196
  });
2036
2197
  transactionState.set(studioId, true);
2037
2198
  return {
@@ -2428,4 +2589,4 @@ async function startServer(port = 3200, options = {}) {
2428
2589
  export {
2429
2590
  startServer
2430
2591
  };
2431
- //# sourceMappingURL=chunk-T5QDC4DR.js.map
2592
+ //# sourceMappingURL=chunk-3FU4T3TX.js.map