breakpoint-mcp 1.0.0 → 1.1.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/config.js CHANGED
@@ -1,28 +1,4 @@
1
1
  import { pathToFileURL } from "node:url";
2
- import { log } from "./logger.js";
3
- /**
4
- * Env vars were renamed CLAUDE_* → BREAKPOINT_* as part of the project rebrand.
5
- * The old names still work for one deprecation cycle: BREAKPOINT_* takes
6
- * precedence, and a set CLAUDE_* is honoured as a fallback with a one-time
7
- * stderr deprecation warning. Drop the fallback (and this helper) once the
8
- * deprecation window closes. GODOT_* vars are unaffected.
9
- */
10
- const _warnedDeprecatedEnv = new Set();
11
- export function envCompat(newName, oldName) {
12
- const current = process.env[newName];
13
- if (current !== undefined)
14
- return current;
15
- const legacy = process.env[oldName];
16
- if (legacy !== undefined) {
17
- if (!_warnedDeprecatedEnv.has(oldName)) {
18
- _warnedDeprecatedEnv.add(oldName);
19
- log(`env ${oldName} is deprecated; use ${newName} instead ` +
20
- `(${oldName} support will be removed in a future release)`);
21
- }
22
- return legacy;
23
- }
24
- return undefined;
25
- }
26
2
  export function loadConfig() {
27
3
  const projectPath = process.env.GODOT_PROJECT ?? process.cwd();
28
4
  // The C# project defaults to the main project, but is usually pointed at a
@@ -32,9 +8,9 @@ export function loadConfig() {
32
8
  godotBin: process.env.GODOT_BIN ?? "godot",
33
9
  projectPath,
34
10
  projectUri: pathToFileURL(projectPath).href,
35
- bridgeHost: envCompat("BREAKPOINT_BRIDGE_HOST", "CLAUDE_BRIDGE_HOST") ?? "127.0.0.1",
36
- bridgePort: Number.parseInt(envCompat("BREAKPOINT_BRIDGE_PORT", "CLAUDE_BRIDGE_PORT") ?? "9080", 10),
37
- bridgeTimeoutMs: Number.parseInt(envCompat("BREAKPOINT_BRIDGE_TIMEOUT_MS", "CLAUDE_BRIDGE_TIMEOUT_MS") ?? "15000", 10),
11
+ bridgeHost: process.env.BREAKPOINT_BRIDGE_HOST ?? "127.0.0.1",
12
+ bridgePort: Number.parseInt(process.env.BREAKPOINT_BRIDGE_PORT ?? "9080", 10),
13
+ bridgeTimeoutMs: Number.parseInt(process.env.BREAKPOINT_BRIDGE_TIMEOUT_MS ?? "15000", 10),
38
14
  lspHost: process.env.GODOT_LSP_HOST ?? "127.0.0.1",
39
15
  lspPort: Number.parseInt(process.env.GODOT_LSP_PORT ?? "6005", 10),
40
16
  lspTimeoutMs: Number.parseInt(process.env.GODOT_LSP_TIMEOUT_MS ?? "15000", 10),
@@ -58,9 +34,9 @@ export function loadConfig() {
58
34
  csDapTimeoutMs: Number.parseInt(process.env.GODOT_CSDAP_TIMEOUT_MS ?? "20000", 10),
59
35
  csDapSetVarTimeoutMs: Number.parseInt(process.env.GODOT_CSDAP_SETVAR_TIMEOUT_MS ?? "8000", 10),
60
36
  csDapEvaluateTimeoutMs: Number.parseInt(process.env.GODOT_CSDAP_EVALUATE_TIMEOUT_MS ?? "8000", 10),
61
- runtimeHost: envCompat("BREAKPOINT_RUNTIME_HOST", "CLAUDE_RUNTIME_HOST") ?? "127.0.0.1",
62
- runtimePort: Number.parseInt(envCompat("BREAKPOINT_RUNTIME_PORT", "CLAUDE_RUNTIME_PORT") ?? "9081", 10),
63
- runtimeTimeoutMs: Number.parseInt(envCompat("BREAKPOINT_RUNTIME_TIMEOUT_MS", "CLAUDE_RUNTIME_TIMEOUT_MS") ?? "15000", 10),
37
+ runtimeHost: process.env.BREAKPOINT_RUNTIME_HOST ?? "127.0.0.1",
38
+ runtimePort: Number.parseInt(process.env.BREAKPOINT_RUNTIME_PORT ?? "9081", 10),
39
+ runtimeTimeoutMs: Number.parseInt(process.env.BREAKPOINT_RUNTIME_TIMEOUT_MS ?? "15000", 10),
64
40
  // Group J: asset generation is OFF by default (backend "none" → tools degrade).
65
41
  assetGenBackend: process.env.BREAKPOINT_ASSETGEN_BACKEND ?? "none",
66
42
  assetGenCommand: process.env.BREAKPOINT_ASSETGEN_CMD ?? "",
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { envCompat, loadConfig } from "./config.js";
4
+ import { loadConfig } from "./config.js";
5
5
  import { BridgeClient } from "./bridge.js";
6
6
  import { LspClient } from "./lsp.js";
7
7
  import { CsLspClient } from "./cslsp.js";
@@ -43,7 +43,7 @@ async function main() {
43
43
  // so long jobs (export/import/headless script) support poll/await/cancel.
44
44
  // D3: also advertise resources.subscribe so clients can subscribe to
45
45
  // godot://… resources and receive notifications/resources/updated.
46
- const server = new McpServer({ name: "breakpoint-mcp", version: "1.0.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
46
+ const server = new McpServer({ name: "breakpoint-mcp", version: "1.1.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
47
47
  // B1: enforce frozen output schemas on every structured tool. Must run before
48
48
  // the register*Tools calls below — it wraps server.registerTool.
49
49
  applyOutputSchemas(server);
@@ -82,7 +82,7 @@ async function main() {
82
82
  // subscribed godot://… resource changes (editor selection / edited scene, or
83
83
  // the running game's live SceneTree). Rapid changes are coalesced per-URI; the
84
84
  // trailing window is overridable via BREAKPOINT_RESOURCE_COALESCE_MS.
85
- const coalesceRaw = envCompat("BREAKPOINT_RESOURCE_COALESCE_MS", "CLAUDE_RESOURCE_COALESCE_MS");
85
+ const coalesceRaw = process.env.BREAKPOINT_RESOURCE_COALESCE_MS;
86
86
  const coalesceMs = coalesceRaw ? Number.parseInt(coalesceRaw, 10) : undefined;
87
87
  registerResourceSubscriptions(server, bridge, runtime, undefined, { coalesceMs });
88
88
  const transport = new StdioServerTransport();
@@ -0,0 +1,162 @@
1
+ import { z } from "zod";
2
+ import { gate } from "../../confirm.js";
3
+ /** AnimationPlayer / Animation authoring + AnimationTree state machines. */
4
+ export function registerAnimationTools(server, call) {
5
+ server.registerTool("anim_player_create", {
6
+ title: "Create AnimationPlayer",
7
+ description: "Add an AnimationPlayer node under a parent (undoable). Seeds an empty default animation library so anim_create works immediately.",
8
+ inputSchema: {
9
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
10
+ name: z.string().optional().describe("Node name (default \"AnimationPlayer\")"),
11
+ },
12
+ }, async ({ parent_path, name }) => call("anim.player_create", { parent_path, name }));
13
+ server.registerTool("anim_create", {
14
+ title: "Create animation",
15
+ description: "Create an empty Animation in an AnimationPlayer's library (undoable). Creates the library if it does not exist yet.",
16
+ inputSchema: {
17
+ player_path: z.string().describe("AnimationPlayer node path relative to the scene root"),
18
+ name: z.string().describe("Animation name (unique within its library)"),
19
+ library: z.string().optional().describe("Animation library name (default \"\", the player's default library)"),
20
+ },
21
+ }, async ({ player_path, name, library }) => call("anim.create", { player_path, name, library: library ?? "" }));
22
+ server.registerTool("anim_delete", {
23
+ title: "Delete animation",
24
+ description: "Delete an Animation from an AnimationPlayer's library (undoable). DESTRUCTIVE — gated by confirmation.",
25
+ inputSchema: {
26
+ player_path: z.string().describe("AnimationPlayer node path relative to the scene root"),
27
+ name: z.string().describe("Animation name"),
28
+ library: z.string().optional().describe("Animation library name (default \"\")"),
29
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
30
+ },
31
+ }, async ({ player_path, name, library, confirm }) => {
32
+ const blocked = await gate(server, confirm, `Delete animation "${name}"`);
33
+ if (blocked)
34
+ return blocked;
35
+ return call("anim.delete", { player_path, name, library: library ?? "" });
36
+ });
37
+ server.registerTool("anim_add_track", {
38
+ title: "Add animation track",
39
+ description: "Add a track to an Animation and set its target path (undoable). Returns the new track index.",
40
+ inputSchema: {
41
+ player_path: z.string().describe("AnimationPlayer node path"),
42
+ name: z.string().describe("Animation name"),
43
+ path: z.string().describe("Track target: a node path, or \"Node:property\" for value tracks (e.g. \"Sprite2D:position\")"),
44
+ type: z
45
+ .string()
46
+ .optional()
47
+ .describe("Track type: value (default), position_3d, rotation_3d, scale_3d, blend_shape, method, bezier, audio, animation"),
48
+ library: z.string().optional().describe("Animation library name (default \"\")"),
49
+ },
50
+ }, async ({ player_path, name, path, type, library }) => call("anim.add_track", { player_path, name, path, type: type ?? "value", library: library ?? "" }));
51
+ server.registerTool("anim_insert_key", {
52
+ title: "Insert animation key",
53
+ description: "Insert a keyframe on a track at a given time (undoable). A key already at that exact time is overwritten.",
54
+ inputSchema: {
55
+ player_path: z.string().describe("AnimationPlayer node path"),
56
+ name: z.string().describe("Animation name"),
57
+ track: z.number().int().describe("Track index"),
58
+ time: z.number().describe("Key time in seconds"),
59
+ value: z.any().describe("Key value (JSON scalar, array, object, or a __type__-tagged Variant matching the track type)"),
60
+ transition: z.number().optional().describe("Transition curve exponent (default 1.0)"),
61
+ library: z.string().optional().describe("Animation library name (default \"\")"),
62
+ },
63
+ }, async ({ player_path, name, track, time, value, transition, library }) => call("anim.insert_key", { player_path, name, track, time, value, transition: transition ?? 1.0, library: library ?? "" }));
64
+ server.registerTool("anim_remove_key", {
65
+ title: "Remove animation key",
66
+ description: "Remove a keyframe by index from a track (undoable).",
67
+ inputSchema: {
68
+ player_path: z.string().describe("AnimationPlayer node path"),
69
+ name: z.string().describe("Animation name"),
70
+ track: z.number().int().describe("Track index"),
71
+ key: z.number().int().describe("Key index within the track"),
72
+ library: z.string().optional().describe("Animation library name (default \"\")"),
73
+ },
74
+ }, async ({ player_path, name, track, key, library }) => call("anim.remove_key", { player_path, name, track, key, library: library ?? "" }));
75
+ server.registerTool("anim_set_length", {
76
+ title: "Set animation length",
77
+ description: "Set an Animation's length in seconds (undoable).",
78
+ inputSchema: {
79
+ player_path: z.string().describe("AnimationPlayer node path"),
80
+ name: z.string().describe("Animation name"),
81
+ length: z.number().describe("New length in seconds (> 0)"),
82
+ library: z.string().optional().describe("Animation library name (default \"\")"),
83
+ },
84
+ }, async ({ player_path, name, length, library }) => call("anim.set_length", { player_path, name, length, library: library ?? "" }));
85
+ server.registerTool("anim_set_loop", {
86
+ title: "Set animation loop mode",
87
+ description: "Set an Animation's loop mode (undoable).",
88
+ inputSchema: {
89
+ player_path: z.string().describe("AnimationPlayer node path"),
90
+ name: z.string().describe("Animation name"),
91
+ mode: z.string().describe("Loop mode: none, linear, or pingpong"),
92
+ library: z.string().optional().describe("Animation library name (default \"\")"),
93
+ },
94
+ }, async ({ player_path, name, mode, library }) => call("anim.set_loop", { player_path, name, mode, library: library ?? "" }));
95
+ server.registerTool("anim_get_track_keys", {
96
+ title: "Get animation track keys",
97
+ description: "List all keyframes on a track (index, time, value, transition). Read-only.",
98
+ inputSchema: {
99
+ player_path: z.string().describe("AnimationPlayer node path"),
100
+ name: z.string().describe("Animation name"),
101
+ track: z.number().int().describe("Track index"),
102
+ library: z.string().optional().describe("Animation library name (default \"\")"),
103
+ },
104
+ }, async ({ player_path, name, track, library }) => call("anim.get_track_keys", { player_path, name, track, library: library ?? "" }));
105
+ server.registerTool("anim_list", {
106
+ title: "List animations",
107
+ description: "List all animations in an AnimationPlayer across its libraries, with length, loop mode, and track count. Read-only.",
108
+ inputSchema: {
109
+ player_path: z.string().describe("AnimationPlayer node path"),
110
+ },
111
+ }, async ({ player_path }) => call("anim.list", { player_path }));
112
+ server.registerTool("anim_tree_create", {
113
+ title: "Create AnimationTree",
114
+ description: "Add an AnimationTree node under a parent (undoable) with a fresh tree_root graph. root_type \"blend_tree\" (AnimationNodeBlendTree) or \"state_machine\" (AnimationNodeStateMachine). Created inactive by default; set anim_player_path to the AnimationPlayer it should drive.",
115
+ inputSchema: {
116
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
117
+ name: z.string().optional().describe("Node name (default \"AnimationTree\")"),
118
+ root_type: z.enum(["blend_tree", "state_machine"]).optional().describe("tree_root graph type (default blend_tree)"),
119
+ anim_player_path: z.string().optional().describe("NodePath to the AnimationPlayer this tree drives, relative to the AnimationTree node"),
120
+ active: z.boolean().optional().describe("Whether the tree processes immediately (default false)"),
121
+ },
122
+ }, async ({ parent_path, name, root_type, anim_player_path, active }) => call("anim.tree_create", { parent_path, name, root_type: root_type ?? "blend_tree", anim_player_path: anim_player_path ?? "", active: active ?? false }));
123
+ server.registerTool("anim_tree_add_node", {
124
+ title: "Add AnimationTree graph node",
125
+ description: "Add a node to an AnimationTree's tree_root graph (AnimationNodeBlendTree or AnimationNodeStateMachine), undoable. node_type is any AnimationNode subclass (e.g. AnimationNodeAnimation, AnimationNodeBlend2, AnimationNodeStateMachine). For AnimationNodeAnimation, pass animation to bind a clip.",
126
+ inputSchema: {
127
+ tree_path: z.string().describe("AnimationTree node path relative to the scene root"),
128
+ node_name: z.string().describe("Unique node name within the graph"),
129
+ node_type: z.string().describe("AnimationNode subclass to instantiate (e.g. AnimationNodeAnimation)"),
130
+ animation: z.string().optional().describe("For AnimationNodeAnimation: the animation name to play"),
131
+ position: z.array(z.number()).optional().describe("Graph editor position [x, y] (default [0, 0])"),
132
+ },
133
+ }, async ({ tree_path, node_name, node_type, animation, position }) => call("anim.tree_add_node", { tree_path, node_name, node_type, animation, position }));
134
+ server.registerTool("anim_statemachine_add_state", {
135
+ title: "Add state machine state",
136
+ description: "Add a state to an AnimationNodeStateMachine (undoable). Targets the AnimationTree's tree_root when it is a state machine, or a nested state-machine node via state_machine. Defaults the state to an AnimationNodeAnimation; pass animation to bind a clip.",
137
+ inputSchema: {
138
+ tree_path: z.string().describe("AnimationTree node path relative to the scene root"),
139
+ state_name: z.string().describe("Unique state name within the state machine"),
140
+ animation: z.string().optional().describe("For an AnimationNodeAnimation state: the animation name to play"),
141
+ node_type: z.string().optional().describe("AnimationNode subclass for the state (default AnimationNodeAnimation)"),
142
+ state_machine: z.string().optional().describe("Name of a nested AnimationNodeStateMachine node within tree_root; omit to target tree_root itself"),
143
+ position: z.array(z.number()).optional().describe("Graph editor position [x, y] (default [0, 0])"),
144
+ },
145
+ }, async ({ tree_path, state_name, animation, node_type, state_machine, position }) => call("anim.statemachine_add_state", { tree_path, state_name, animation, node_type: node_type ?? "AnimationNodeAnimation", state_machine: state_machine ?? "", position }));
146
+ server.registerTool("anim_statemachine_add_transition", {
147
+ title: "Add state machine transition",
148
+ description: "Add a transition between two states in an AnimationNodeStateMachine (undoable). from_state/to_state must exist (or be the built-in \"Start\"/\"End\"). switch_mode: immediate|sync|at_end; advance_mode: disabled|enabled|auto.",
149
+ inputSchema: {
150
+ tree_path: z.string().describe("AnimationTree node path relative to the scene root"),
151
+ from_state: z.string().describe("Source state name (or \"Start\")"),
152
+ to_state: z.string().describe("Destination state name (or \"End\")"),
153
+ state_machine: z.string().optional().describe("Name of a nested AnimationNodeStateMachine node within tree_root; omit to target tree_root itself"),
154
+ xfade_time: z.number().optional().describe("Cross-fade time in seconds (default 0)"),
155
+ switch_mode: z.enum(["immediate", "sync", "at_end"]).optional().describe("Switch mode (default immediate)"),
156
+ advance_mode: z.enum(["disabled", "enabled", "auto"]).optional().describe("Advance mode (default enabled)"),
157
+ advance_condition: z.string().optional().describe("Advance condition parameter name (used with advance_mode auto)"),
158
+ priority: z.number().int().optional().describe("Transition priority (lower wins when multiple are valid)"),
159
+ },
160
+ }, async ({ tree_path, from_state, to_state, state_machine, xfade_time, switch_mode, advance_mode, advance_condition, priority }) => call("anim.statemachine_add_transition", { tree_path, from_state, to_state, state_machine: state_machine ?? "", xfade_time: xfade_time ?? 0.0, switch_mode: switch_mode ?? "immediate", advance_mode: advance_mode ?? "enabled", advance_condition: advance_condition ?? "", priority }));
161
+ }
162
+ //# sourceMappingURL=animation.js.map
@@ -0,0 +1,109 @@
1
+ import { z } from "zod";
2
+ import { gate } from "../../confirm.js";
3
+ /** AudioStreamPlayer nodes + audio bus layout / effects. */
4
+ export function registerAudioTools(server, call) {
5
+ server.registerTool("audio_player_create", {
6
+ title: "Create audio player",
7
+ description: "Add an AudioStreamPlayer / AudioStreamPlayer2D / AudioStreamPlayer3D node under a parent in the edited scene (undoable). `dim` selects \"none\" (default, non-positional AudioStreamPlayer), \"2d\", or \"3d\". Optional initial stream_path (res:// AudioStream), autoplay, volume_db, bus. In-scene and undoable — not gated.",
8
+ inputSchema: {
9
+ parent_path: z.string().describe("Parent node path relative to the scene root; \".\" for the root"),
10
+ dim: z.enum(["none", "2d", "3d"]).optional().describe("Player kind: \"none\" (default, AudioStreamPlayer), \"2d\" (AudioStreamPlayer2D), or \"3d\" (AudioStreamPlayer3D)"),
11
+ name: z.string().optional().describe("Node name (default matches the player class)"),
12
+ stream_path: z.string().optional().describe("res:// path to an AudioStream to assign to the new player"),
13
+ autoplay: z.boolean().optional().describe("Whether the player starts automatically on scene load"),
14
+ volume_db: z.number().optional().describe("Player volume in dB"),
15
+ bus: z.string().optional().describe("Target audio bus name (default \"Master\")"),
16
+ },
17
+ }, async ({ parent_path, dim, name, stream_path, autoplay, volume_db, bus }) => {
18
+ const params = { parent_path };
19
+ if (dim !== undefined)
20
+ params.dim = dim;
21
+ if (name !== undefined)
22
+ params.name = name;
23
+ if (stream_path !== undefined)
24
+ params.stream_path = stream_path;
25
+ if (autoplay !== undefined)
26
+ params.autoplay = autoplay;
27
+ if (volume_db !== undefined)
28
+ params.volume_db = volume_db;
29
+ if (bus !== undefined)
30
+ params.bus = bus;
31
+ return call("audio.player_create", params);
32
+ });
33
+ server.registerTool("audio_set_stream", {
34
+ title: "Set audio stream",
35
+ description: "Load an AudioStream from a res:// path and assign it as stream on an AudioStreamPlayer/2D/3D in the edited scene (undoable). In-scene and undoable — not gated.",
36
+ inputSchema: {
37
+ path: z.string().describe("AudioStreamPlayer/2D/3D node path relative to the scene root"),
38
+ stream_path: z.string().describe("res:// path to an AudioStream resource"),
39
+ },
40
+ }, async ({ path, stream_path }) => call("audio.set_stream", { path, stream_path }));
41
+ server.registerTool("audio_bus_add", {
42
+ title: "Add audio bus",
43
+ description: "Add a bus to the project's global AudioServer layout, optionally naming it, positioning it (at_position), and setting its send bus. DESTRUCTIVE (project-wide audio state) — gated by confirmation.",
44
+ inputSchema: {
45
+ name: z.string().optional().describe("Name for the new bus"),
46
+ at_position: z.number().optional().describe("Insert index (default -1 = append at end)"),
47
+ send: z.string().optional().describe("Name of the bus this bus sends its output to"),
48
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
49
+ },
50
+ }, async ({ name, at_position, send, confirm }) => {
51
+ const blocked = await gate(server, confirm, `Add audio bus${name ? ` "${name}"` : ""}`);
52
+ if (blocked)
53
+ return blocked;
54
+ const params = {};
55
+ if (name !== undefined)
56
+ params.name = name;
57
+ if (at_position !== undefined)
58
+ params.at_position = at_position;
59
+ if (send !== undefined)
60
+ params.send = send;
61
+ return call("audio.bus_add", params);
62
+ });
63
+ server.registerTool("audio_bus_add_effect", {
64
+ title: "Add audio bus effect",
65
+ description: "Instantiate an AudioEffect subclass (by class name, e.g. \"AudioEffectReverb\") and add it to a named bus on the global AudioServer, optionally at a position in the bus's effect chain. DESTRUCTIVE (project-wide audio state) — gated by confirmation.",
66
+ inputSchema: {
67
+ bus: z.string().describe("Target bus name"),
68
+ effect: z.string().describe("AudioEffect subclass name, e.g. \"AudioEffectReverb\", \"AudioEffectDelay\""),
69
+ at_position: z.number().optional().describe("Insert index within the bus's effect chain (default -1 = append)"),
70
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
71
+ },
72
+ }, async ({ bus, effect, at_position, confirm }) => {
73
+ const blocked = await gate(server, confirm, `Add ${effect} to audio bus "${bus}"`);
74
+ if (blocked)
75
+ return blocked;
76
+ const params = { bus, effect };
77
+ if (at_position !== undefined)
78
+ params.at_position = at_position;
79
+ return call("audio.bus_add_effect", params);
80
+ });
81
+ server.registerTool("audio_bus_set_volume", {
82
+ title: "Set audio bus volume",
83
+ description: "Set the volume (in dB) of a named bus on the global AudioServer. DESTRUCTIVE (project-wide audio state) — gated by confirmation.",
84
+ inputSchema: {
85
+ bus: z.string().describe("Bus name"),
86
+ volume_db: z.number().describe("Volume in dB"),
87
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
88
+ },
89
+ }, async ({ bus, volume_db, confirm }) => {
90
+ const blocked = await gate(server, confirm, `Set audio bus "${bus}" volume to ${volume_db} dB`);
91
+ if (blocked)
92
+ return blocked;
93
+ return call("audio.bus_set_volume", { bus, volume_db });
94
+ });
95
+ server.registerTool("audio_set_bus_layout", {
96
+ title: "Save audio bus layout",
97
+ description: "Save the current AudioServer bus layout (buses, effects, volumes) to a .tres resource on disk (default res://default_bus_layout.tres) via generate_bus_layout + ResourceSaver.save. DESTRUCTIVE (writes a file) — gated by confirmation.",
98
+ inputSchema: {
99
+ to_path: z.string().optional().describe("Destination res:// path (default res://default_bus_layout.tres)"),
100
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
101
+ },
102
+ }, async ({ to_path, confirm }) => {
103
+ const blocked = await gate(server, confirm, `Save audio bus layout to ${to_path ?? "res://default_bus_layout.tres"}`);
104
+ if (blocked)
105
+ return blocked;
106
+ return call("audio.set_bus_layout", to_path !== undefined ? { to_path } : {});
107
+ });
108
+ }
109
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1,31 @@
1
+ import { ok } from "../lsp-common.js";
2
+ /**
3
+ * MCP error envelope for a failed editor-bridge call (never throws to the
4
+ * caller). Distinct from lsp-common's `fail` (which labels errors "LSP error");
5
+ * this one labels them "Bridge error".
6
+ */
7
+ export function fail(err) {
8
+ const be = err;
9
+ const code = be?.code ?? "error";
10
+ const message = be?.message ?? String(err);
11
+ return {
12
+ isError: true,
13
+ content: [{ type: "text", text: `Bridge error [${code}]: ${message}` }],
14
+ };
15
+ }
16
+ /**
17
+ * Build the shared bridge-call helper used by every editor tool group: forward
18
+ * a method to the in-editor addon over TCP and wrap the result in the standard
19
+ * MCP success envelope, or a friendly Bridge-error envelope when unreachable.
20
+ */
21
+ export function makeCall(bridge) {
22
+ return async (method, params = {}) => {
23
+ try {
24
+ return ok(await bridge.request(method, params));
25
+ }
26
+ catch (err) {
27
+ return fail(err);
28
+ }
29
+ };
30
+ }
31
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { gate } from "../../confirm.js";
3
+ /** Editor session + core project settings (editor_* / project get/set). */
4
+ export function registerCoreTools(server, call) {
5
+ server.registerTool("editor_ping", { title: "Ping editor bridge", description: "Check that the editor is running with the Breakpoint MCP plugin enabled.", inputSchema: {} }, async () => call("ping"));
6
+ server.registerTool("editor_get_state", { title: "Editor state", description: "Return the currently edited scene, its root type/path, and the current node selection.", inputSchema: {} }, async () => call("editor.get_state"));
7
+ server.registerTool("editor_undo", {
8
+ title: "Undo last edit",
9
+ description: "Undo the most recent edit in the editor's undo history (like Ctrl-Z). Defaults to the edited scene's history; pass scope='global' for the editor-wide history. Returns whether an action was undone, its name, and the remaining undo/redo depth.",
10
+ inputSchema: {
11
+ scope: z
12
+ .enum(["scene", "global"])
13
+ .optional()
14
+ .describe("Which history to step: 'scene' (default, the edited scene) or 'global' (editor-wide)"),
15
+ },
16
+ }, async ({ scope }) => call("edit.undo", scope ? { scope } : {}));
17
+ server.registerTool("editor_redo", {
18
+ title: "Redo last undone edit",
19
+ description: "Re-apply the most recently undone edit in the editor's undo history (like Ctrl-Shift-Z). Defaults to the edited scene's history; pass scope='global' for the editor-wide history.",
20
+ inputSchema: {
21
+ scope: z
22
+ .enum(["scene", "global"])
23
+ .optional()
24
+ .describe("Which history to step: 'scene' (default, the edited scene) or 'global' (editor-wide)"),
25
+ },
26
+ }, async ({ scope }) => call("edit.redo", scope ? { scope } : {}));
27
+ server.registerTool("project_get_info", { title: "Project info", description: "Return project name, main scene, project root path, Godot version, and feature tags.", inputSchema: {} }, async () => call("project.get_info"));
28
+ server.registerTool("project_get_setting", {
29
+ title: "Get project setting",
30
+ description: "Read a single ProjectSettings value by dotted key (e.g. application/config/name).",
31
+ inputSchema: { name: z.string().describe("ProjectSettings key") },
32
+ }, async ({ name }) => call("project.get_setting", { name }));
33
+ server.registerTool("project_set_setting", {
34
+ title: "Set project setting",
35
+ description: "Write a ProjectSettings value. Set save=true to persist to project.godot. DESTRUCTIVE — gated by confirmation.",
36
+ inputSchema: {
37
+ name: z.string().describe("ProjectSettings key"),
38
+ value: z.any().describe("New value; rich types use the {\"__type__\":...} tagging convention"),
39
+ save: z.boolean().optional().describe("Persist to disk (default false)"),
40
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
41
+ },
42
+ }, async ({ name, value, save, confirm }) => {
43
+ const blocked = await gate(server, confirm, `Set project setting "${name}"${save ? " and save project.godot" : ""}`);
44
+ if (blocked)
45
+ return blocked;
46
+ return call("project.set_setting", { name, value, save: save ?? false });
47
+ });
48
+ }
49
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import { gate } from "../../confirm.js";
3
+ /** Project FileSystem dock ops: list / scan / move / mkdir. */
4
+ export function registerFilesystemTools(server, call) {
5
+ server.registerTool("filesystem_list", {
6
+ title: "List project directory",
7
+ description: "List the subdirectories and files of a directory in the project filesystem (hidden entries like .godot are skipped). Read-only.",
8
+ inputSchema: { path: z.string().optional().describe("Directory res:// path (default res://)") },
9
+ }, async ({ path }) => call("filesystem.list", path !== undefined ? { path } : {}));
10
+ server.registerTool("filesystem_scan", {
11
+ title: "Rescan filesystem",
12
+ description: "Trigger an editor rescan of the project filesystem so newly added or externally-changed files are picked up. Read-only side effect.",
13
+ inputSchema: {},
14
+ }, async () => call("filesystem.scan"));
15
+ server.registerTool("filesystem_move", {
16
+ title: "Move / rename file",
17
+ description: "Move or rename a file or directory within the project (carrying its .import sidecar), then rescan. DESTRUCTIVE (moves on disk; does NOT remap references in other resources) — gated by confirmation.",
18
+ inputSchema: {
19
+ from_path: z.string().describe("Source res:// path (file or directory)"),
20
+ to_path: z.string().describe("Destination res:// path"),
21
+ confirm: z.boolean().optional().describe("Auto-approve this destructive action (skip the confirmation prompt)"),
22
+ },
23
+ }, async ({ from_path, to_path, confirm }) => {
24
+ const blocked = await gate(server, confirm, `Move ${from_path} to ${to_path}`);
25
+ if (blocked)
26
+ return blocked;
27
+ return call("filesystem.move", { from_path, to_path });
28
+ });
29
+ server.registerTool("filesystem_create_dir", {
30
+ title: "Create directory",
31
+ description: "Create a directory (recursively) in the project filesystem, then rescan. No-op if it already exists.",
32
+ inputSchema: { path: z.string().describe("Directory res:// path to create") },
33
+ }, async ({ path }) => call("filesystem.create_dir", { path }));
34
+ }
35
+ //# sourceMappingURL=filesystem.js.map
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import { fail } from "./common.js";
3
+ /** Editor selection, ClassDB / docs lookups, and viewport screenshot. */
4
+ export function registerIntrospectionTools(server, call, bridge) {
5
+ server.registerTool("selection_get", { title: "Get selection", description: "Return the paths of the nodes currently selected in the editor.", inputSchema: {} }, async () => call("selection.get"));
6
+ server.registerTool("selection_set", {
7
+ title: "Set selection",
8
+ description: "Replace the editor's node selection with the given node paths.",
9
+ inputSchema: { paths: z.array(z.string()).describe("Node paths relative to the scene root") },
10
+ }, async ({ paths }) => call("selection.set", { paths }));
11
+ server.registerTool("classdb_get_class", {
12
+ title: "Introspect class",
13
+ description: "Return the parent class, methods, properties, and signals of an engine class via ClassDB.",
14
+ inputSchema: {
15
+ class_name: z.string().describe("Engine class name, e.g. AudioStreamPlayer3D"),
16
+ include_inherited: z.boolean().optional().describe("Include inherited members (default false)"),
17
+ },
18
+ }, async ({ class_name, include_inherited }) => call("classdb.get_class", { class_name, include_inherited: include_inherited ?? false }));
19
+ server.registerTool("class_reference", {
20
+ title: "Class reference",
21
+ description: "Full engine-class reference via ClassDB: method SIGNATURES (return type + typed args), signal " +
22
+ "signatures, and typed properties — the detailed view classdb_get_class summarises as bare names. " +
23
+ "Read-only. Includes the canonical online docs URL. Pass member to filter to a single method/property/signal.",
24
+ inputSchema: {
25
+ class_name: z.string().describe("Engine class name, e.g. AudioStreamPlayer3D"),
26
+ include_inherited: z.boolean().optional().describe("Include inherited members (default false)"),
27
+ member: z.string().optional().describe("Only return members whose name contains this substring"),
28
+ },
29
+ }, async ({ class_name, include_inherited, member }) => call("classdb.reference", {
30
+ class_name,
31
+ include_inherited: include_inherited ?? false,
32
+ member: member ?? "",
33
+ }));
34
+ server.registerTool("docs_search", {
35
+ title: "Search the class reference",
36
+ description: "Search the Godot class reference (ClassDB) by keyword — matching class names and, unless a scope narrows " +
37
+ "it, their methods/properties/signals — and return each hit with its canonical docs URL. Read-only. " +
38
+ "Use kind to restrict to one member type, class_name to scope to a single class, and limit to bound results.",
39
+ inputSchema: {
40
+ query: z.string().describe("Case-insensitive substring to match against class / member names"),
41
+ kind: z.enum(["any", "class", "method", "property", "signal"]).optional().describe("Restrict to one result kind (default any)"),
42
+ class_name: z.string().optional().describe("Scope the member search to a single class (still returns class-name matches project-wide)"),
43
+ limit: z.number().int().positive().optional().describe("Max results before truncation (default 40)"),
44
+ deep: z.boolean().optional().describe("Also scan members, not just class names (default true)"),
45
+ },
46
+ }, async ({ query, kind, class_name, limit, deep }) => call("docs.search", {
47
+ query,
48
+ kind: kind ?? "any",
49
+ class_name: class_name ?? "",
50
+ limit: limit ?? 40,
51
+ deep: deep ?? true,
52
+ }));
53
+ server.registerTool("screenshot_editor", {
54
+ title: "Screenshot editor viewport",
55
+ description: "Capture the 2D or 3D editor viewport as a PNG and return it as image content so the assistant can see the scene. " +
56
+ "Requires the matching editor tab (2D/3D) to be active and rendered.",
57
+ inputSchema: { viewport: z.enum(["2d", "3d"]).optional().describe("Which viewport (default 3d)") },
58
+ }, async ({ viewport }) => {
59
+ try {
60
+ const r = (await bridge.request("screenshot.editor_viewport", { viewport: viewport ?? "3d" }));
61
+ return {
62
+ content: [
63
+ { type: "image", data: r.base64, mimeType: r.mime },
64
+ { type: "text", text: `Captured ${r.viewport} viewport (${r.width}x${r.height}).` },
65
+ ],
66
+ };
67
+ }
68
+ catch (err) {
69
+ return fail(err);
70
+ }
71
+ });
72
+ }
73
+ //# sourceMappingURL=introspection.js.map