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.
- package/README.md +2 -2
- package/addon/breakpoint_mcp/bridge_secret.gd +61 -0
- package/addon/breakpoint_mcp/bridge_server.gd +66 -4
- package/addon/breakpoint_mcp/operations.gd +1 -1
- package/addon/breakpoint_mcp/pause_latch.gd +65 -0
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/addon/breakpoint_mcp/plugin.gd +6 -0
- package/addon/breakpoint_mcp/runtime_bridge.gd +334 -5
- package/addon/breakpoint_mcp/status_dock.gd +48 -0
- package/dist/bridge.js +22 -1
- package/dist/capabilities.js +150 -0
- package/dist/cli/clients.js +5 -2
- package/dist/cli/doctor.js +41 -0
- package/dist/cli/init.js +48 -6
- package/dist/config.js +64 -0
- package/dist/confirm.js +21 -0
- package/dist/index.js +72 -65
- package/dist/pause.js +105 -0
- package/dist/recipes.js +168 -0
- package/dist/schemas.js +43 -0
- package/dist/secret.js +38 -0
- package/dist/tools/runtime.js +112 -0
- package/dist/toolsets.js +44 -0
- package/package.json +1 -1
|
@@ -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
|
package/dist/cli/clients.js
CHANGED
|
@@ -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
|
|
42
|
-
|
|
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
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
12
|
-
import {
|
|
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.
|
|
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
|
-
//
|
|
40
|
+
// any register*Tools call — it wraps server.registerTool.
|
|
51
41
|
applyOutputSchemas(server);
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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).
|
|
96
|
-
//
|
|
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
|
-
|
|
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
|
|
119
|
+
processes?.killAll();
|
|
113
120
|
process.exit(0);
|
|
114
121
|
};
|
|
115
122
|
process.on("SIGINT", shutdown);
|