breakpoint-mcp 1.12.1 → 1.14.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The MCP **host** for [Breakpoint MCP](https://github.com/jlivingston-Cipher/godot-breakpoint-mcp) —
4
4
  a [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
5
5
  Godot game engine to AI coding assistants across four planes: headless CLI, the live
6
- editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **270 tools
6
+ editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **271 tools
7
7
  + 5 MCP resources**, built against the stable `@modelcontextprotocol/sdk` 1.x API.
8
8
  Developed and tested with **Claude**; because MCP is an open protocol, other clients can
9
9
  connect too (currently untested — reports welcome).
@@ -85,6 +85,8 @@ func dispatch(method: String, params: Dictionary) -> Dictionary:
85
85
  return _node_change_type(params)
86
86
  "node.set_owner":
87
87
  return _node_set_owner(params)
88
+ "node.set_editable_instance":
89
+ return _node_set_editable_instance(params)
88
90
  "node.call_method":
89
91
  return _node_call_method(params)
90
92
  "node.get_path":
@@ -1129,6 +1131,40 @@ func _node_set_owner(params: Dictionary) -> Dictionary:
1129
1131
  return _ok({"path": _path_of(root, node), "owner": owner_out})
1130
1132
 
1131
1133
 
1134
+ ## Toggle "Editable Children" on an instanced sub-scene. When enabled, property
1135
+ ## overrides on the instance's INTERNAL nodes serialize into the owning scene on
1136
+ ## save (otherwise the sub-scene is sealed and its internals revert on reload).
1137
+ ## The flag lives on the owning scene's state, so it is set on the instance's
1138
+ ## owner (the scene root that will save it), which is where is_editable_instance
1139
+ ## reads it. This is what lets card_instance bake author-time slot data.
1140
+ func _node_set_editable_instance(params: Dictionary) -> Dictionary:
1141
+ var root := _edited_root()
1142
+ if root == null:
1143
+ return _err("no_scene", "No scene is open")
1144
+ var node := _resolve(root, String(params.get("path", "")))
1145
+ if node == null:
1146
+ return _err("bad_path", "Node not found: %s" % params.get("path", ""))
1147
+ if node == root:
1148
+ return _err("refused", "The scene root is not an instance")
1149
+ if node.scene_file_path == "":
1150
+ return _err("refused", "Node is not an instanced sub-scene (no scene_file_path); editable-instance applies only to scene instances")
1151
+ var owner_node: Node = node.owner
1152
+ if owner_node == null:
1153
+ return _err("refused", "Node has no owner; it is not saved into a scene, so editable-instance has nothing to record against")
1154
+ var editable := bool(params.get("editable", true))
1155
+ var old_editable := owner_node.is_editable_instance(node)
1156
+ var ur := _plugin.get_undo_redo()
1157
+ ur.create_action("Breakpoint: set editable-instance of %s" % node.name)
1158
+ ur.add_do_method(owner_node, "set_editable_instance", node, editable)
1159
+ ur.add_undo_method(owner_node, "set_editable_instance", node, old_editable)
1160
+ ur.commit_action()
1161
+ return _ok({
1162
+ "path": _path_of(root, node),
1163
+ "editable": owner_node.is_editable_instance(node),
1164
+ "owner": _path_of(root, owner_node),
1165
+ })
1166
+
1167
+
1132
1168
  func _node_call_method(params: Dictionary) -> Dictionary:
1133
1169
  var root := _edited_root()
1134
1170
  if root == null:
package/dist/schemas.js CHANGED
@@ -127,6 +127,7 @@ export const outputSchemas = {
127
127
  node_move_child: { path: z.string(), index: z.number() },
128
128
  node_change_type: { path: z.string(), name: z.string(), type: z.string(), old_type: z.string() },
129
129
  node_set_owner: { path: z.string(), owner: z.string().nullable() },
130
+ node_set_editable_instance: { path: z.string(), editable: z.boolean(), owner: z.string() },
130
131
  node_call_method: { path: z.string(), method: z.string(), result: encodedValue },
131
132
  node_get_path: {
132
133
  path: z.string(), name: z.string(), type: z.string(),
@@ -686,11 +687,13 @@ export const outputSchemas = {
686
687
  face_up: z.boolean(),
687
688
  bound: z.array(z.string()),
688
689
  unbound: z.array(z.string()),
690
+ persisted: z.boolean(),
689
691
  },
690
692
  card_hand_layout: {
691
693
  container_path: z.string(),
692
694
  mode: z.string(),
693
695
  count: z.number(),
696
+ persisted: z.boolean(),
694
697
  instances: z.array(z.object({ index: z.number(), instance_path: z.string() })),
695
698
  },
696
699
  card_deck_from_table: {
@@ -699,6 +702,7 @@ export const outputSchemas = {
699
702
  rows_read: z.number(),
700
703
  rows_skipped: z.number(),
701
704
  unmapped_columns: z.array(z.string()),
705
+ persisted: z.boolean(),
702
706
  instances: z.array(z.object({ row_index: z.number(), instance_path: z.string() })),
703
707
  },
704
708
  card_set_face: {
@@ -769,6 +773,7 @@ export const outputSchemas = {
769
773
  unbound: z.array(z.string()),
770
774
  placed: z.boolean(),
771
775
  cell: z.string().nullable(),
776
+ persisted: z.boolean(),
772
777
  },
773
778
  piece_move: {
774
779
  moved: z.boolean(),
@@ -141,6 +141,16 @@ export function registerNodeTools(server, call) {
141
141
  owner_path: z.string().optional().describe("Ancestor to own the node; \".\" or omitted for the scene root"),
142
142
  },
143
143
  }, async ({ path, owner_path }) => call("node.set_owner", owner_path !== undefined ? { path, owner_path } : { path }));
144
+ server.registerTool("node_set_editable_instance", {
145
+ title: "Set editable-instance (editable children)",
146
+ description: "Toggle \"Editable Children\" on an instanced sub-scene (undoable). When enabled, property overrides on the instance's " +
147
+ "internal nodes serialize into the saved scene; without it the sub-scene is sealed and its internals revert on reload. " +
148
+ "This is what lets author-time edits (e.g. card_instance slot data) be baked into the .tscn.",
149
+ inputSchema: {
150
+ path: z.string().describe("Node path of the instanced sub-scene, relative to the scene root"),
151
+ editable: z.boolean().optional().describe("Enable (true, default) or disable editable children"),
152
+ },
153
+ }, async ({ path, editable }) => call("node.set_editable_instance", { path, editable: editable ?? true }));
144
154
  server.registerTool("node_call_method", {
145
155
  title: "Call node method (edit-time)",
146
156
  description: "Invoke a method on a node in the EDITED scene. DESTRUCTIVE (arbitrary invocation, not undoable) — gated by confirmation. " +
@@ -479,30 +479,46 @@ async function emitOneCard(emit, args) {
479
479
  }
480
480
  const res = await emit("node.call_method", { path: instPath, method: "set_data", args: [args.data] });
481
481
  await emit("node.call_method", { path: instPath, method: "set_face", args: [args.face_up] });
482
- return { instance_path: instPath, ...splitFromCall(res, args.data) };
482
+ // Finding-A save-persistence (opt-in). set_data mutates the instance's internal
483
+ // slot nodes, but a sealed sub-scene doesn't serialize those overrides, so the
484
+ // data reverts on reload. Enabling "Editable Children" makes the overrides
485
+ // serialize into the saved scene → authored card data survives a reload.
486
+ // Default off preserves the lighter runtime-bound contract (cards populated at
487
+ // load via set_data), which is correct for gameplay decks.
488
+ const persisted = args.persist === true;
489
+ if (persisted) {
490
+ await emit("node.set_editable_instance", { path: instPath, editable: true });
491
+ }
492
+ return { instance_path: instPath, ...splitFromCall(res, args.data), persisted };
483
493
  }
484
494
  export async function emitCardInstance(emit, args) {
485
495
  const face_up = args.face_up ?? true;
486
496
  const name = args.name ?? sceneRootName(args.template_path);
487
- const { instance_path, bound, unbound } = await emitOneCard(emit, {
497
+ const { instance_path, bound, unbound, persisted } = await emitOneCard(emit, {
488
498
  template_path: args.template_path, parent: args.parent, data: args.data, name, face_up,
489
499
  placement: args.position ? { x: args.position.x, y: args.position.y } : undefined,
500
+ persist: args.persist,
490
501
  });
491
- return { instance_path, face_up, bound, unbound };
502
+ return { instance_path, face_up, bound, unbound, persisted };
492
503
  }
493
504
  export async function emitCardHand(emit, args) {
494
505
  const container = args.parent === "" ? "." : args.parent;
495
506
  const base = sceneRootName(args.template_path);
496
507
  const places = computeLayout(args.mode, args.cards.length, args);
508
+ // Finding-A save-persistence (opt-in), threaded per-card through emitOneCard: when
509
+ // set, every card in the hand gets Editable Children so its bound slot data serializes
510
+ // on save instead of reverting on reload. Default off keeps the runtime-bound contract.
511
+ const persisted = args.persist === true;
497
512
  const instances = [];
498
513
  for (let i = 0; i < args.cards.length; i++) {
499
514
  const { instance_path } = await emitOneCard(emit, {
500
515
  template_path: args.template_path, parent: container, data: args.cards[i].data,
501
516
  name: `${base}_${i}`, face_up: args.cards[i].face_up ?? true, placement: places[i],
517
+ persist: args.persist,
502
518
  });
503
519
  instances.push({ index: i, instance_path });
504
520
  }
505
- return { container_path: container, mode: args.mode, count: instances.length, instances };
521
+ return { container_path: container, mode: args.mode, count: instances.length, persisted, instances };
506
522
  }
507
523
  export async function emitDeckFromTable(emit, readFile, args) {
508
524
  const container = args.parent === "" ? "." : args.parent;
@@ -547,13 +563,15 @@ export async function emitDeckFromTable(emit, readFile, args) {
547
563
  const { instance_path } = await emitOneCard(emit, {
548
564
  template_path: args.template_path, parent: container, data, name, face_up,
549
565
  placement: args.layout ? places[i] : undefined,
566
+ persist: args.persist,
550
567
  });
551
568
  instances.push({ row_index: allRows.indexOf(row), instance_path });
552
569
  }
553
570
  const unmapped_columns = [...header].filter((c) => !referenced.has(c)).sort();
554
571
  return {
555
572
  deck_container: container, count: instances.length, rows_read,
556
- rows_skipped: rows_read - instances.length, unmapped_columns, instances,
573
+ rows_skipped: rows_read - instances.length, unmapped_columns,
574
+ persisted: args.persist === true, instances,
557
575
  };
558
576
  }
559
577
  /**
@@ -1012,13 +1030,25 @@ export async function emitPieceInstance(emit, args) {
1012
1030
  const res = await emit("node.call_method", { path: instPath, method: "set_data", args: [args.data] });
1013
1031
  await emit("node.call_method", { path: instPath, method: "set_face", args: [face_up] });
1014
1032
  const { bound, unbound } = splitFromCall(res, args.data);
1033
+ // Finding-A save-persistence (opt-in), mirroring card_instance. set_data mutates the
1034
+ // piece's internal Art/Label nodes, but a sealed sub-scene doesn't serialize those
1035
+ // overrides, so they revert on reload. Enabling "Editable Children" bakes them into the
1036
+ // saved scene. Emitted last, on the piece's final resting path (after any place_on
1037
+ // reparent). Default off preserves the lighter runtime-bound contract.
1038
+ const persisted = args.persist === true;
1015
1039
  if (args.place_on) {
1016
1040
  const placed = await emitBoardPlace(emit, {
1017
1041
  board: args.place_on.board, cell: args.place_on.cell, node: instPath, align: args.place_on.align,
1018
1042
  });
1019
- return { instance_path: placed.node_path, face_up, bound, unbound, placed: true, cell: args.place_on.cell };
1043
+ if (persisted) {
1044
+ await emit("node.set_editable_instance", { path: placed.node_path, editable: true });
1045
+ }
1046
+ return { instance_path: placed.node_path, face_up, bound, unbound, placed: true, cell: args.place_on.cell, persisted };
1047
+ }
1048
+ if (persisted) {
1049
+ await emit("node.set_editable_instance", { path: instPath, editable: true });
1020
1050
  }
1021
- return { instance_path: instPath, face_up, bound, unbound, placed: false, cell: null };
1051
+ return { instance_path: instPath, face_up, bound, unbound, placed: false, cell: null, persisted };
1022
1052
  }
1023
1053
  /**
1024
1054
  * Move a piece onto a destination cell (reusing `board_place` for the reparent +
@@ -1380,6 +1410,7 @@ export function registerTabletopTools(server, bridge, config) {
1380
1410
  position: z.object({ x: z.number(), y: z.number() }).optional().describe("Local position of the instance"),
1381
1411
  face_up: z.boolean().optional().describe("Show the face (default true); false shows the back on two-sided cards"),
1382
1412
  name: z.string().optional().describe("Optional node name for the instance"),
1413
+ persist: z.boolean().optional().describe("Bake the bound slot data into the saved scene by enabling Editable Children on the instance (default false = runtime-bound only, reverts on reload)"),
1383
1414
  },
1384
1415
  }, async (raw) => {
1385
1416
  const a = raw;
@@ -1404,6 +1435,7 @@ export function registerTabletopTools(server, bridge, config) {
1404
1435
  })).min(1).describe("One entry per card to instance"),
1405
1436
  mode: z.enum(["row", "fan", "stack", "grid"]).describe("Arrangement mode"),
1406
1437
  ...layoutKnobs,
1438
+ persist: z.boolean().optional().describe("Bake each card's bound slot data into the saved scene by enabling Editable Children on every instanced card (default false = runtime-bound only, reverts on reload)"),
1407
1439
  },
1408
1440
  }, async (raw) => {
1409
1441
  const a = raw;
@@ -1440,6 +1472,7 @@ export function registerTabletopTools(server, bridge, config) {
1440
1472
  align: z.enum(["start", "center", "end"]).optional(),
1441
1473
  origin: z.object({ x: z.number(), y: z.number() }).optional(),
1442
1474
  }).optional().describe("Optional arrangement (same knobs as card_hand_layout); omitted → stacked at origin"),
1475
+ persist: z.boolean().optional().describe("Bake each stamped card's bound slot data into the saved scene by enabling Editable Children on every card (default false = runtime-bound only, reverts on reload)"),
1443
1476
  },
1444
1477
  }, async (raw) => {
1445
1478
  const a = raw;
@@ -1665,6 +1698,7 @@ export function registerTabletopTools(server, bridge, config) {
1665
1698
  cell: z.string().describe("Cell id to place onto (resolves to <board>/cell_<cell>)"),
1666
1699
  align: z.object({ x: z.number(), y: z.number() }).optional().describe("Offset from the cell origin in px (default centred)"),
1667
1700
  }).optional().describe("Optionally place the new piece on a board cell in the same call"),
1701
+ persist: z.boolean().optional().describe("Bake the bound data into the saved scene by enabling Editable Children on the instance (default false = runtime-bound only, reverts on reload)"),
1668
1702
  },
1669
1703
  }, async (raw) => {
1670
1704
  const a = raw;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breakpoint-mcp",
3
- "version": "1.12.1",
3
+ "version": "1.14.0",
4
4
  "description": "MCP server exposing Godot to AI coding assistants over the Model Context Protocol — developed and tested with Claude — across four planes (CLI, live editor, LSP+DAP, runtime) with elicitation-gated destructive tools, long jobs on the formal MCP task model, captured console output, and MCP resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",