breakpoint-mcp 1.14.0 → 1.19.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.
@@ -0,0 +1,150 @@
1
+ /** The two groups, in display order. */
2
+ export const CAPABILITY_GROUPS = ["code-execution", "network"];
3
+ /** One-line human description per group (shown by `doctor` and the resource). */
4
+ export const GROUP_DESCRIBE = {
5
+ "code-execution": "Run arbitrary GDScript, invoke arbitrary methods, evaluate an expression in a paused debug frame, or run a local asset-gen command backend.",
6
+ network: "Egress beyond loopback — the Group M backend SDK.",
7
+ };
8
+ /**
9
+ * Tool → capability group(s). A tool absent from this map is unprivileged and
10
+ * always registered. This is the single source of truth for the risk tagging,
11
+ * asserted total-and-correct by `capabilities.test.ts`.
12
+ */
13
+ export const TOOL_CAPABILITIES = {
14
+ // code-execution — arbitrary execution / invocation / paused-frame evaluation
15
+ godot_run_headless_script: ["code-execution"],
16
+ godot_run_managed: ["code-execution"],
17
+ node_call_method: ["code-execution"],
18
+ runtime_call_method: ["code-execution"],
19
+ dbg_evaluate: ["code-execution"],
20
+ cs_dbg_evaluate: ["code-execution"],
21
+ // asset generation — the local command backend (code-execution) is the only
22
+ // privileged path. Formerly also tagged `network` for an external provider
23
+ // backend, but that backend is not implemented, so the network tag is dropped
24
+ // until it ships — keeps the advertised capability matching the real surface.
25
+ asset_gen_configure: ["code-execution"],
26
+ asset_gen_icon: ["code-execution"],
27
+ asset_gen_sprite: ["code-execution"],
28
+ asset_gen_texture: ["code-execution"],
29
+ asset_gen_model: ["code-execution"],
30
+ asset_gen_audio_sfx: ["code-execution"],
31
+ // network — Group M backend SDK (egress to a backend provider)
32
+ backend_configure: ["network"],
33
+ backend_detect: ["network"],
34
+ };
35
+ /**
36
+ * Parse the raw BREAKPOINT_PRIVILEGED_GROUPS env into a normalized token list
37
+ * (or null for "unset" → no groups → the safe-default surface). Comma/whitespace
38
+ * separated, lower-cased. Mirrors `parseToolsets`.
39
+ */
40
+ export function parsePrivilegedGroups(raw) {
41
+ if (raw == null)
42
+ return null;
43
+ const toks = raw
44
+ .split(/[,\s]+/)
45
+ .map((t) => t.trim().toLowerCase())
46
+ .filter(Boolean);
47
+ return toks.length ? toks : null;
48
+ }
49
+ /**
50
+ * Resolve normalized tokens to the enabled group set. Unknown tokens are
51
+ * reported via `onUnknown` and ignored (a typo never silently enables a group).
52
+ * `null`/empty → the empty set (safe default). `all` enables every group.
53
+ */
54
+ export function selectPrivilegedGroups(tokens, onUnknown) {
55
+ const enabled = new Set();
56
+ if (!tokens)
57
+ return enabled;
58
+ const known = new Set(CAPABILITY_GROUPS);
59
+ const unknown = [];
60
+ for (const t of tokens) {
61
+ if (t === "all") {
62
+ for (const g of CAPABILITY_GROUPS)
63
+ enabled.add(g);
64
+ }
65
+ else if (known.has(t)) {
66
+ enabled.add(t);
67
+ }
68
+ else {
69
+ unknown.push(t);
70
+ }
71
+ }
72
+ if (unknown.length && onUnknown)
73
+ onUnknown(unknown);
74
+ return enabled;
75
+ }
76
+ /** Is a tool allowed given the enabled groups? Untagged tools are always allowed. */
77
+ export function toolAllowed(name, enabled) {
78
+ const groups = TOOL_CAPABILITIES[name];
79
+ if (!groups || groups.length === 0)
80
+ return true;
81
+ return groups.some((g) => enabled.has(g));
82
+ }
83
+ /** The sorted set of privileged tool names dropped when `enabled` groups are active. */
84
+ export function droppedTools(enabled) {
85
+ return Object.keys(TOOL_CAPABILITIES)
86
+ .filter((name) => !toolAllowed(name, enabled))
87
+ .sort();
88
+ }
89
+ /**
90
+ * Wrap `server.registerTool` to DROP any tool whose capability group isn't
91
+ * enabled, so a disabled group's tools never reach `tools/list`. Mirrors
92
+ * `applyOutputSchemas`' wrapping; call once, right AFTER `applyOutputSchemas`
93
+ * (so the schema-injection wrapper stays innermost) and before any
94
+ * `register*Tools()` call. A dropped tool returns a harmless stub handle so the
95
+ * calling register* code proceeds unchanged.
96
+ */
97
+ export function applyCapabilities(server, enabled) {
98
+ const gate = (raw) => (name, config, handler) => {
99
+ if (!toolAllowed(name, enabled)) {
100
+ // Not registered — omitted from tools/list entirely (least-privilege).
101
+ return { name };
102
+ }
103
+ return raw(name, config, handler);
104
+ };
105
+ const s = server;
106
+ s.registerTool = gate(s.registerTool.bind(server));
107
+ // D2 task-model tools (long jobs like `godot_run_headless_script`) register
108
+ // through server.experimental.tasks.registerToolTask, NOT registerTool — gate
109
+ // that path too, or a privileged task tool would slip past the drop filter.
110
+ const tasks = s.experimental?.tasks;
111
+ if (tasks?.registerToolTask) {
112
+ tasks.registerToolTask = gate(tasks.registerToolTask.bind(tasks));
113
+ }
114
+ }
115
+ /**
116
+ * Register the always-on `godot://capabilities` resource: a read-only listing of
117
+ * the capability groups, their enabled/disabled state, exactly which tools each
118
+ * gates, the dropped set, and the env one-liner to enable them. Registered
119
+ * UNCONDITIONALLY (not behind the `resources` toolset), so the dropped privileged
120
+ * tools are never a silent gap — an agent can always see what exists-but-is-
121
+ * disabled and how to turn it on.
122
+ */
123
+ export function registerCapabilitiesResource(server, enabled) {
124
+ server.registerResource("capabilities", "godot://capabilities", {
125
+ title: "Capability groups",
126
+ description: "Higher-trust tool groups, their enabled/disabled state, the tools each gates, and how to enable them.",
127
+ mimeType: "application/json",
128
+ }, async (uri) => {
129
+ const groups = CAPABILITY_GROUPS.map((g) => ({
130
+ id: g,
131
+ enabled: enabled.has(g),
132
+ describe: GROUP_DESCRIBE[g],
133
+ tools: Object.keys(TOOL_CAPABILITIES)
134
+ .filter((name) => (TOOL_CAPABILITIES[name] ?? []).includes(g))
135
+ .sort(),
136
+ }));
137
+ const payload = {
138
+ summary: "Two higher-trust tool groups are OFF by default. A disabled group's tools are not registered (omitted from tools/list). Enable a group to load its tools.",
139
+ default_secure: enabled.size === 0,
140
+ enabled_groups: [...enabled].sort(),
141
+ dropped_tools: droppedTools(enabled),
142
+ how_to_enable: "Set BREAKPOINT_PRIVILEGED_GROUPS in the MCP server env (comma-separated): 'code-execution', 'network', 'code-execution,network', or 'all'. Or re-run `npx breakpoint-mcp init` for a guided setup.",
143
+ groups,
144
+ };
145
+ return {
146
+ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(payload, null, 2) }],
147
+ };
148
+ });
149
+ }
150
+ //# sourceMappingURL=capabilities.js.map
@@ -38,11 +38,14 @@ export function clientInfo(id, projectPath) {
38
38
  return null;
39
39
  }
40
40
  }
41
- /** The stdio server entry a client config needs. GODOT_BIN is included only when non-default. */
42
- export function serverEntry(projectPath, godotBin, needsType) {
41
+ /** The stdio server entry a client config needs. GODOT_BIN is included only when
42
+ * non-default; BREAKPOINT_PRIVILEGED_GROUPS only when higher-trust groups are opted in. */
43
+ export function serverEntry(projectPath, godotBin, needsType, privilegedGroups) {
43
44
  const env = { GODOT_PROJECT: projectPath };
44
45
  if (godotBin && godotBin !== "godot")
45
46
  env.GODOT_BIN = godotBin;
47
+ if (privilegedGroups)
48
+ env.BREAKPOINT_PRIVILEGED_GROUPS = privilegedGroups;
46
49
  const base = { command: "npx", args: ["-y", "breakpoint-mcp"], env };
47
50
  return needsType ? { type: "stdio", ...base } : base;
48
51
  }
@@ -22,6 +22,7 @@ import fs from "node:fs";
22
22
  import path from "node:path";
23
23
  import { spawnSync } from "node:child_process";
24
24
  import { loadConfig } from "../config.js";
25
+ import { CAPABILITY_GROUPS, droppedTools, selectPrivilegedGroups } from "../capabilities.js";
25
26
  import { parseArgs } from "./args.js";
26
27
  const ADDON_REL = "addons/breakpoint_mcp";
27
28
  const PLUGIN_CFG_RES = "res://addons/breakpoint_mcp/plugin.cfg";
@@ -175,8 +176,48 @@ function checkCsharpTool(name, cmd) {
175
176
  detail: `${cmd} not on PATH (C# plane inactive until installed)`,
176
177
  };
177
178
  }
179
+ /**
180
+ * Report the capability-group state (`code-execution` / `network`), how many
181
+ * higher-trust tools the current setting drops, and how to change it — plus a
182
+ * signal hint when an asset-gen backend is configured but its group is off (so
183
+ * the "why won't asset-gen run?" gap the drop model can create is surfaced, not
184
+ * silent). Informational only — never a required failure.
185
+ */
186
+ export function checkCapabilities(config) {
187
+ const enabled = selectPrivilegedGroups(config.privilegedGroups);
188
+ const dropped = droppedTools(enabled);
189
+ const state = CAPABILITY_GROUPS.map((g) => `${g} ${enabled.has(g) ? "on" : "off"}`).join(" · ");
190
+ const checks = [
191
+ {
192
+ name: "capability-groups",
193
+ status: "ok",
194
+ severity: "info",
195
+ detail: dropped.length === 0
196
+ ? `${state} — full 276-tool surface`
197
+ : `${state} (secure default) — ${dropped.length} higher-trust tool(s) dropped from the surface`,
198
+ hint: dropped.length === 0
199
+ ? undefined
200
+ : "Enable with BREAKPOINT_PRIVILEGED_GROUPS=code-execution,network (or `breakpoint-mcp init --trust full`). See the godot://capabilities resource for the exact tool list.",
201
+ },
202
+ ];
203
+ const assetGenConfigured = (config.assetGenBackend && config.assetGenBackend !== "none") ||
204
+ Boolean(config.assetGenCommand) ||
205
+ Boolean(config.assetGenProvider);
206
+ if (assetGenConfigured && !enabled.has("code-execution")) {
207
+ checks.push({
208
+ name: "capability-assetgen",
209
+ status: "ok",
210
+ severity: "info",
211
+ detail: "asset-gen backend configured, but code-execution is off — the asset_gen_* tools are not loaded",
212
+ hint: "Enable it: BREAKPOINT_PRIVILEGED_GROUPS=code-execution (the local command backend is the only privileged asset-gen path).",
213
+ });
214
+ }
215
+ return checks;
216
+ }
178
217
  export async function runDoctorChecks(config, opts) {
179
218
  const checks = [];
219
+ // Capability groups — the secure-default surface + how to widen it (info only).
220
+ checks.push(...checkCapabilities(config));
180
221
  // Godot binary (give the version probe a floor so a slow cold start isn't a false negative).
181
222
  checks.push(checkGodotBinary(config.godotBin, Math.max(opts.timeoutMs, 3000)));
182
223
  // Editor addon install + enable.
package/dist/cli/init.js CHANGED
@@ -15,6 +15,30 @@ import { fileURLToPath } from "node:url";
15
15
  import { parseArgs } from "./args.js";
16
16
  import { CLIENT_IDS, clientInfo, mergeClientConfig, serverEntry, snippet } from "./clients.js";
17
17
  import { DEFAULT_REPO, fetchAddonFromGitHub } from "./github.js";
18
+ import { CAPABILITY_GROUPS, parsePrivilegedGroups, selectPrivilegedGroups } from "../capabilities.js";
19
+ /**
20
+ * Resolve the guided trust preset into a validated BREAKPOINT_PRIVILEGED_GROUPS
21
+ * value ("" = the safe default — `code-execution` + `network` off). Accepts either
22
+ * `--trust safe|full` (friendly presets) or `--privileged-groups <list>`
23
+ * (`code-execution`, `network`, `all`, comma-separated). Unknown tokens are dropped
24
+ * with a warning so a typo never silently enables a group.
25
+ */
26
+ export function resolvePrivilegedGroups(flags) {
27
+ const trustAlias = { safe: "", none: "", off: "", full: "all", all: "all" };
28
+ const raw = typeof flags["privileged-groups"] === "string"
29
+ ? flags["privileged-groups"]
30
+ : typeof flags.trust === "string"
31
+ ? (trustAlias[flags.trust.toLowerCase()] ?? flags.trust)
32
+ : "";
33
+ if (!raw)
34
+ return { value: "" };
35
+ let warn;
36
+ const enabled = selectPrivilegedGroups(parsePrivilegedGroups(raw), (unknown) => {
37
+ warn = `ignoring unknown trust group(s): ${unknown.join(", ")} (valid: ${CAPABILITY_GROUPS.join(", ")}, all)`;
38
+ });
39
+ const value = [...enabled].sort().join(",");
40
+ return warn ? { value, warn } : { value };
41
+ }
18
42
  const PLUGIN_REL = "addons/breakpoint_mcp";
19
43
  const PLUGIN_CFG_RES = "res://addons/breakpoint_mcp/plugin.cfg";
20
44
  const SERVER_NAME = "godot";
@@ -117,8 +141,9 @@ export function installAddon(addonSource, projectPath, opts) {
117
141
  fs.cpSync(addonSource, dest, { recursive: true });
118
142
  return { action: exists ? "overwritten" : "installed", dest };
119
143
  }
120
- function claudeCodeCommand(projectPath) {
121
- return `claude mcp add godot --env GODOT_PROJECT=${projectPath} -- npx -y breakpoint-mcp`;
144
+ function claudeCodeCommand(projectPath, privilegedGroups) {
145
+ const groups = privilegedGroups ? ` --env BREAKPOINT_PRIVILEGED_GROUPS=${privilegedGroups}` : "";
146
+ return `claude mcp add godot --env GODOT_PROJECT=${projectPath}${groups} -- npx -y breakpoint-mcp`;
122
147
  }
123
148
  /** Entry point for `breakpoint-mcp init`. Returns the process exit code. */
124
149
  export async function runInit(argv, deps = {}) {
@@ -130,6 +155,8 @@ export async function runInit(argv, deps = {}) {
130
155
  const force = flags.force === true;
131
156
  const client = typeof flags.client === "string" ? flags.client : "none";
132
157
  const godotBin = process.env.GODOT_BIN ?? "godot";
158
+ // Guided trust preset → BREAKPOINT_PRIVILEGED_GROUPS ("" = safe default).
159
+ const { value: privilegedGroups, warn: privilegedWarn } = resolvePrivilegedGroups(flags);
133
160
  // --from-github [ref]: source the addon from GitHub instead of the bundled copy.
134
161
  const fromGitHub = "from-github" in flags;
135
162
  const fgVal = flags["from-github"];
@@ -212,14 +239,14 @@ export async function runInit(argv, deps = {}) {
212
239
  say("");
213
240
  if (client === "claude-code") {
214
241
  say("MCP client (Claude Code) — run:");
215
- say(` ${claudeCodeCommand(projectPath)}`);
242
+ say(` ${claudeCodeCommand(projectPath, privilegedGroups)}`);
216
243
  }
217
244
  else if (client === "none") {
218
245
  say('MCP client — add this to your client config (wrapper key "mcpServers"):');
219
- say(snippet("mcpServers", SERVER_NAME, serverEntry(projectPath, godotBin, false)));
246
+ say(snippet("mcpServers", SERVER_NAME, serverEntry(projectPath, godotBin, false, privilegedGroups)));
220
247
  say("");
221
248
  say("Claude Code users can instead run:");
222
- say(` ${claudeCodeCommand(projectPath)}`);
249
+ say(` ${claudeCodeCommand(projectPath, privilegedGroups)}`);
223
250
  }
224
251
  else {
225
252
  const info = clientInfo(client, projectPath);
@@ -227,7 +254,7 @@ export async function runInit(argv, deps = {}) {
227
254
  process.stderr.write(`init: unknown --client '${client}'. Known: ${CLIENT_IDS.join(", ")}.\n`);
228
255
  return 1;
229
256
  }
230
- const entry = serverEntry(projectPath, godotBin, info.needsType);
257
+ const entry = serverEntry(projectPath, godotBin, info.needsType, privilegedGroups);
231
258
  if (dryRun) {
232
259
  say(`MCP client (${info.label}): would write ${info.configPath}`);
233
260
  }
@@ -249,6 +276,21 @@ export async function runInit(argv, deps = {}) {
249
276
  say(`MCP client (${info.label}): ${existed ? "updated" : "created"} ${info.configPath}${existed ? " (backup at .bak)" : ""}`);
250
277
  }
251
278
  }
279
+ // Trust groups — surface the secure default so even a bare `init` teaches it.
280
+ say("");
281
+ if (privilegedGroups) {
282
+ say(`Higher-trust tool groups enabled: ${privilegedGroups} — the full 276-tool surface loads.`);
283
+ }
284
+ else {
285
+ say("Higher-trust tool groups are OFF by default (the secure default). The assistant loads the");
286
+ say("everyday authoring/debug surface and drops the 14 code-execution + network tools (e.g.");
287
+ say("godot_run_headless_script, the *_call_method / dbg_evaluate tools, asset-gen, backend_*).");
288
+ say("Enable them by re-running with `--trust full` (or `--privileged-groups code-execution,network`),");
289
+ say("or set BREAKPOINT_PRIVILEGED_GROUPS in the server env. The `godot://capabilities` resource and");
290
+ say("`breakpoint-mcp doctor` list exactly what each group gates.");
291
+ }
292
+ if (privilegedWarn)
293
+ say(` note: ${privilegedWarn}`);
252
294
  say("");
253
295
  say("Next: open the project in Godot, then run `breakpoint-mcp doctor --require-live` to verify.");
254
296
  process.stdout.write(out.join("\n") + "\n");
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { pathToFileURL } from "node:url";
2
+ import { parsePrivilegedGroups } from "./capabilities.js";
2
3
  export function loadConfig() {
3
4
  const projectPath = process.env.GODOT_PROJECT ?? process.cwd();
4
5
  // The C# project defaults to the main project, but is usually pointed at a
@@ -42,6 +43,69 @@ export function loadConfig() {
42
43
  assetGenCommand: process.env.BREAKPOINT_ASSETGEN_CMD ?? "",
43
44
  assetGenProvider: process.env.BREAKPOINT_ASSETGEN_PROVIDER ?? "",
44
45
  assetGenTimeoutMs: Number.parseInt(process.env.BREAKPOINT_ASSETGEN_TIMEOUT_MS ?? "120000", 10),
46
+ toolsets: parseToolsets(process.env.BREAKPOINT_TOOLSETS),
47
+ privilegedGroups: parsePrivilegedGroups(process.env.BREAKPOINT_PRIVILEGED_GROUPS),
45
48
  };
46
49
  }
50
+ /** Parse the raw BREAKPOINT_TOOLSETS env into a normalized token list (or null
51
+ * for "unset" → full surface). Comma/whitespace separated, lower-cased. */
52
+ export function parseToolsets(raw) {
53
+ if (raw == null)
54
+ return null;
55
+ const toks = raw
56
+ .split(/[,\s]+/)
57
+ .map((t) => t.trim().toLowerCase())
58
+ .filter(Boolean);
59
+ return toks.length ? toks : null;
60
+ }
61
+ /**
62
+ * Plane/convenience aliases → concrete toolset ids. Kept here (not in
63
+ * `toolsets.ts`) so selection has no dependency on the register* functions and
64
+ * stays trivially unit-testable.
65
+ */
66
+ export const TOOLSET_ALIASES = {
67
+ a: ["editor"],
68
+ b: ["cli"],
69
+ c: ["runtime"],
70
+ d: ["lsp", "cslsp", "dap", "csdap"],
71
+ csharp: ["cslsp", "csdap"],
72
+ semantic: ["lsp", "cslsp", "dap", "csdap"],
73
+ };
74
+ /**
75
+ * Resolve a requested toolset list against the known ids.
76
+ * - `requested == null` → every id (the default full surface).
77
+ * - aliases expand to their ids; unknown tokens are dropped (reported via
78
+ * `onUnknown`, so `index.ts` can warn without this being impure).
79
+ * - if nothing valid resolves, fall back to the full surface (a misconfigured
80
+ * filter must never silently yield an empty, useless server).
81
+ * Returns a Set preserving membership only; the caller iterates the ordered
82
+ * toolset list, so registration order is unaffected.
83
+ */
84
+ export function selectToolsets(allIds, requested, onUnknown) {
85
+ const known = new Set(allIds);
86
+ if (requested == null)
87
+ return new Set(allIds);
88
+ const out = new Set();
89
+ const unknown = [];
90
+ for (const tok of requested) {
91
+ if (tok === "all") {
92
+ for (const id of allIds)
93
+ out.add(id);
94
+ }
95
+ else if (TOOLSET_ALIASES[tok]) {
96
+ for (const id of TOOLSET_ALIASES[tok])
97
+ if (known.has(id))
98
+ out.add(id);
99
+ }
100
+ else if (known.has(tok)) {
101
+ out.add(tok);
102
+ }
103
+ else {
104
+ unknown.push(tok);
105
+ }
106
+ }
107
+ if (unknown.length && onUnknown)
108
+ onUnknown(unknown);
109
+ return out.size ? out : new Set(allIds);
110
+ }
47
111
  //# sourceMappingURL=config.js.map
package/dist/confirm.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { pauseLatch } from "./pause.js";
1
2
  /**
2
3
  * Gate a destructive action behind MCP elicitation (a client-side confirmation
3
4
  * prompt). Returns `null` when the action may proceed, or a blocking tool result
@@ -9,6 +10,26 @@
9
10
  * executed silently on a client that can't ask.
10
11
  */
11
12
  export async function gate(server, confirm, summary) {
13
+ // Track 2 — global-pause overlay. Coarser than the per-tool elicitation below
14
+ // (which stays the lead control): while the operator has the agent paused, hold
15
+ // ENTRY to this mutating action until they resume or the wait times out — then
16
+ // block rather than act. In-flight ops and read-only tools are never affected.
17
+ if (pauseLatch.isPaused()) {
18
+ const resumed = await pauseLatch.awaitResumed();
19
+ if (!resumed) {
20
+ return {
21
+ isError: true,
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: `Paused — the agent is currently paused, so "${summary}" was held and NOT executed. ` +
26
+ `Resume the agent (SIGUSR2), then re-run the tool.`,
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ }
32
+ pauseLatch.record(summary);
12
33
  if (confirm === true)
13
34
  return null;
14
35
  try {
package/dist/index.js CHANGED
@@ -1,36 +1,26 @@
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 { loadConfig } from "./config.js";
4
+ import { loadConfig, selectToolsets } from "./config.js";
5
5
  import { BridgeClient } from "./bridge.js";
6
+ import { resolveBridgeSecret } from "./secret.js";
6
7
  import { LspClient } from "./lsp.js";
7
8
  import { CsLspClient } from "./cslsp.js";
8
9
  import { CsDapClient } from "./csdap.js";
9
10
  import { StdioChannel } from "./stdio.js";
10
11
  import { DapClient } from "./dap.js";
11
- import { registerCliTools } from "./tools/cli.js";
12
- import { registerEditorTools } from "./tools/editor.js";
13
- import { registerLspTools } from "./tools/lsp.js";
14
- import { registerCsLspTools } from "./tools/cslsp.js";
15
- import { registerDapTools } from "./tools/dap.js";
16
- import { registerCsDapTools } from "./tools/csdap.js";
17
- import { registerRuntimeTools } from "./tools/runtime.js";
18
- import { registerProcessTools } from "./tools/processes.js";
19
- import { registerKnowledgeTools } from "./tools/knowledge.js";
20
- import { registerVcsTools } from "./tools/vcs.js";
21
- import { registerAssetGenTools } from "./tools/assetgen.js";
22
- import { registerNetcodeTools } from "./tools/netcode.js";
23
- import { registerBackendTools } from "./tools/backend.js";
24
- import { registerTabletopTools } from "./tools/tabletop.js";
25
- import { registerResources } from "./tools/resources.js";
12
+ import { buildToolsets } from "./toolsets.js";
13
+ import { registerRecipes } from "./recipes.js";
26
14
  import { applyOutputSchemas } from "./schemas.js";
15
+ import { applyCapabilities, droppedTools, registerCapabilitiesResource, selectPrivilegedGroups, } from "./capabilities.js";
27
16
  import { taskStore, TASK_CAPABILITIES } from "./tasks.js";
28
17
  import { RESOURCE_CAPABILITIES, registerResourceSubscriptions } from "./subscriptions.js";
18
+ import { pauseLatch, installPauseSignalHandlers } from "./pause.js";
29
19
  import { log } from "./logger.js";
30
20
  async function main() {
31
21
  const config = loadConfig();
32
- const bridge = new BridgeClient(config.bridgeHost, config.bridgePort, config.bridgeTimeoutMs);
33
- const runtime = new BridgeClient(config.runtimeHost, config.runtimePort, config.runtimeTimeoutMs, "runtime bridge", "Is the project running? Launch it (godot_run_project or dbg_launch) with the Breakpoint MCP plugin enabled — it auto-registers the runtime autoload.");
22
+ const bridge = new BridgeClient(config.bridgeHost, config.bridgePort, config.bridgeTimeoutMs, "editor bridge", undefined, () => resolveBridgeSecret(config.projectPath, ["BREAKPOINT_BRIDGE_SECRET"]));
23
+ const runtime = new BridgeClient(config.runtimeHost, config.runtimePort, config.runtimeTimeoutMs, "runtime bridge", "Is the project running? Launch it (godot_run_project or dbg_launch) with the Breakpoint MCP plugin enabled — it auto-registers the runtime autoload.", () => resolveBridgeSecret(config.projectPath, ["BREAKPOINT_RUNTIME_SECRET", "BREAKPOINT_BRIDGE_SECRET"]));
34
24
  const lsp = new LspClient(config.lspHost, config.lspPort, config.projectUri, config.lspTimeoutMs);
35
25
  // D4 C2: the C# semantic plane. OmniSharp is spawned over stdio (lazily, on the
36
26
  // first cs_* call) against the C# project root — so a host without OmniSharp
@@ -45,58 +35,75 @@ async function main() {
45
35
  // so long jobs (export/import/headless script) support poll/await/cancel.
46
36
  // D3: also advertise resources.subscribe so clients can subscribe to
47
37
  // godot://… resources and receive notifications/resources/updated.
48
- const server = new McpServer({ name: "breakpoint-mcp", version: "1.11.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
38
+ const server = new McpServer({ name: "breakpoint-mcp", version: "1.19.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
49
39
  // B1: enforce frozen output schemas on every structured tool. Must run before
50
- // the register*Tools calls below — it wraps server.registerTool.
40
+ // any register*Tools call — it wraps server.registerTool.
51
41
  applyOutputSchemas(server);
52
- // Plane B (headless CLI): works without the editor running.
53
- registerCliTools(server, config);
54
- // Plane A (live editor): requires the editor open with the Breakpoint MCP plugin.
55
- registerEditorTools(server, bridge);
56
- // Plane D (semantic): connects to Godot's GDScript language server (LSP, 6005).
57
- registerLspTools(server, lsp, config);
58
- // Plane D (C# semantic, D4 C2): drives OmniSharp over stdio for the cs_* tools.
59
- registerCsLspTools(server, csLsp, config);
60
- // Plane D (debugging): connects to Godot's Debug Adapter (DAP, 6006).
61
- registerDapTools(server, dap, config);
62
- // Plane D (C# debugging, D4 C3): drives netcoredbg over stdio for the cs_dbg_* tools.
63
- registerCsDapTools(server, csDap, config);
64
- // Plane C (runtime): connects to the in-game runtime autoload (9081).
65
- registerRuntimeTools(server, runtime);
66
- // Phase 4: managed run + captured console output (transparent print() logs).
67
- const processes = registerProcessTools(server, config);
68
- // Group K: host-side knowledge & search (project grep, symbol/usage index, idiom lookup).
69
- registerKnowledgeTools(server, config);
70
- // Group L: host-side version control (vcs_*). Read-only core over the `git` binary
71
- // (status/log/diff/show/branches/blame) rooted at the project path — no editor, no
72
- // language server, so it answers whenever the project is a git work tree. Mutating
73
- // git tools are deferred pending a scope steer and will reuse the elicitation gate.
74
- registerVcsTools(server, config);
75
- // Group J: AI asset generation (delegated backend / connected client; degrades
76
- // to a request spec when no backend is configured). Writes + imports via the bridge.
77
- registerAssetGenTools(server, bridge, config);
78
- // Group M: native multiplayer & backend scaffolding (mp_*). Pure authoring —
79
- // undoable node ops (spawner/synchronizer/authority) + gated GDScript codegen
80
- // (enet/webrtc peer, @rpc wiring, lobby). Hosts nothing; scaffolds everything.
81
- registerNetcodeTools(server, bridge, config);
82
- // Group M (second half): backend-SDK integration scaffolding (backend_* / *_scaffold).
83
- // Detects the installed SDK (SilentWolf/Nakama/PlayFab/Photon) and generates gated
84
- // GDScript against it; degrades cleanly when the SDK is absent or lacks the feature.
85
- registerBackendTools(server, bridge, config);
86
- // Group N: card/board/piece authoring composites (card_*). Host-side scripted
87
- // sequences of existing editor-bridge primitives (scene/control/node/theme/
88
- // resource) — they build + data-bind scenes, add no addon method, and invent
89
- // no game rules or data.
90
- registerTabletopTools(server, bridge, config);
91
- // Phase 4: MCP resources (scene tree, editor state, runtime tree/log, ClassDB docs).
92
- registerResources(server, bridge, runtime);
42
+ // Capability groups — a risk-based axis over the toolsets. Both `code-execution`
43
+ // and `network` are OFF by default; a disabled group's tools are DROPPED at
44
+ // registration (omitted from tools/list), so the secure-default surface is
45
+ // 276 − 14 = 262 tools. Enable via BREAKPOINT_PRIVILEGED_GROUPS. Wraps
46
+ // server.registerTool AFTER applyOutputSchemas (schema wrapper stays innermost).
47
+ const privilegedGroups = selectPrivilegedGroups(config.privilegedGroups, (unknown) => log(`ignoring unknown BREAKPOINT_PRIVILEGED_GROUPS token(s): ${unknown.join(", ")}`));
48
+ applyCapabilities(server, privilegedGroups);
49
+ // The A/B/C/D planes ARE the grouping. Build the ordered toolset registry
50
+ // (the single source of truth, shared with the registration tests) and
51
+ // register only the selected groups. Default (BREAKPOINT_TOOLSETS unset) =
52
+ // every group → the full 276-tool surface, byte-identical to before. A filter
53
+ // lets a client that can't defer tools, or a user who wants a smaller default
54
+ // menu, load only the planes a project needs (GitHub-MCP `--toolsets` style).
55
+ let processes;
56
+ const toolsets = buildToolsets({
57
+ server,
58
+ bridge,
59
+ runtime,
60
+ lsp,
61
+ csLsp,
62
+ dap,
63
+ csDap,
64
+ config,
65
+ onProcesses: (h) => {
66
+ processes = h;
67
+ },
68
+ });
69
+ const enabled = selectToolsets(toolsets.map((t) => t.id), config.toolsets, (unknown) => log(`ignoring unknown BREAKPOINT_TOOLSETS token(s): ${unknown.join(", ")}`));
70
+ for (const ts of toolsets)
71
+ if (enabled.has(ts.id))
72
+ ts.run();
93
73
  // D3: resource subscriptions — push notifications/resources/updated when a
94
74
  // subscribed godot://… resource changes (editor selection / edited scene, or
95
- // the running game's live SceneTree). Rapid changes are coalesced per-URI; the
96
- // trailing window is overridable via BREAKPOINT_RESOURCE_COALESCE_MS.
75
+ // the running game's live SceneTree). Adds no tools (pure notification
76
+ // plumbing), so it's wired here rather than as a toolset, gated on `resources`.
77
+ // Rapid changes are coalesced per-URI; the trailing window is overridable via
78
+ // BREAKPOINT_RESOURCE_COALESCE_MS.
97
79
  const coalesceRaw = process.env.BREAKPOINT_RESOURCE_COALESCE_MS;
98
80
  const coalesceMs = coalesceRaw ? Number.parseInt(coalesceRaw, 10) : undefined;
99
- registerResourceSubscriptions(server, bridge, runtime, undefined, { coalesceMs });
81
+ if (enabled.has("resources")) {
82
+ registerResourceSubscriptions(server, bridge, runtime, undefined, { coalesceMs });
83
+ }
84
+ if (config.toolsets) {
85
+ log(`toolsets enabled: ${[...enabled].sort().join(", ")} (${enabled.size}/${toolsets.length} groups)`);
86
+ }
87
+ // Recipes: a free, curated task-recipe layer exposed as MCP prompts (discoverable
88
+ // via prompts/list). Adds NO tools — the 276-tool count is unchanged — and drives
89
+ // the enforced tools above, so it's a skill-pack layer over typed/undoable tools.
90
+ registerRecipes(server);
91
+ // Always-on capability affordance — never behind a toolset or a privileged
92
+ // group — so the dropped high-trust tools are never a silent gap: an agent can
93
+ // read godot://capabilities to see what exists-but-is-disabled and how to
94
+ // enable it. Registered directly (not via the `resources` toolset).
95
+ registerCapabilitiesResource(server, privilegedGroups);
96
+ if (config.privilegedGroups) {
97
+ const on = [...privilegedGroups].sort().join(", ") || "(none)";
98
+ log(`privileged groups enabled: ${on}; dropped ${droppedTools(privilegedGroups).length} tool(s) from the surface`);
99
+ }
100
+ // Track 2 — global-pause latch (prototype). A coarse overlay on the destructive
101
+ // gate: SIGUSR1 pauses, SIGUSR2 resumes; BREAKPOINT_START_PAUSED starts held.
102
+ // Per-tool elicitation gating stays the lead control (see src/pause.ts).
103
+ installPauseSignalHandlers();
104
+ if (pauseLatch.isPaused()) {
105
+ log("[pause] started PAUSED (BREAKPOINT_START_PAUSED) — send SIGUSR2 to resume");
106
+ }
100
107
  const transport = new StdioServerTransport();
101
108
  await server.connect(transport);
102
109
  log(`ready · godotBin=${config.godotBin} · project=${config.projectPath} · ` +
@@ -109,7 +116,7 @@ async function main() {
109
116
  csLsp.close();
110
117
  dap.close();
111
118
  csDap.close();
112
- processes.killAll();
119
+ processes?.killAll();
113
120
  process.exit(0);
114
121
  };
115
122
  process.on("SIGINT", shutdown);