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 +1 -1
- package/addon/breakpoint_mcp/operations.gd +36 -0
- package/dist/schemas.js +5 -0
- package/dist/tools/editor/node.js +10 -0
- package/dist/tools/tabletop.js +41 -7
- package/package.json +1 -1
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. **
|
|
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. " +
|
package/dist/tools/tabletop.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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",
|