breakpoint-mcp 1.0.0 → 1.2.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/addon/breakpoint_mcp/README.md +28 -0
- package/addon/breakpoint_mcp/bridge_server.gd +121 -0
- package/addon/breakpoint_mcp/icon.png +0 -0
- package/addon/breakpoint_mcp/operations.gd +4578 -0
- package/addon/breakpoint_mcp/plugin.cfg +7 -0
- package/addon/breakpoint_mcp/plugin.gd +64 -0
- package/addon/breakpoint_mcp/runtime_bridge.gd +440 -0
- package/addon/breakpoint_mcp/variant_json.gd +108 -0
- package/dist/cli/args.js +40 -0
- package/dist/cli/clients.js +74 -0
- package/dist/cli/doctor.js +269 -0
- package/dist/cli/init.js +199 -0
- package/dist/config.js +6 -30
- package/dist/index.js +49 -4
- package/dist/tools/editor/animation.js +162 -0
- package/dist/tools/editor/audio.js +109 -0
- package/dist/tools/editor/common.js +31 -0
- package/dist/tools/editor/core.js +49 -0
- package/dist/tools/editor/filesystem.js +35 -0
- package/dist/tools/editor/introspection.js +73 -0
- package/dist/tools/editor/node.js +171 -0
- package/dist/tools/editor/particles.js +96 -0
- package/dist/tools/editor/physics.js +236 -0
- package/dist/tools/editor/project_input_test.js +187 -0
- package/dist/tools/editor/resource.js +104 -0
- package/dist/tools/editor/scene.js +95 -0
- package/dist/tools/editor/shader.js +59 -0
- package/dist/tools/editor/signal.js +67 -0
- package/dist/tools/editor/spatial.js +185 -0
- package/dist/tools/editor/tiles.js +160 -0
- package/dist/tools/editor/ui.js +175 -0
- package/dist/tools/editor.js +39 -1914
- package/package.json +4 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-client config targets for `breakpoint-mcp init`. Each client launches the
|
|
3
|
+
* server the same way — `npx -y breakpoint-mcp` over stdio — and differs only in
|
|
4
|
+
* the config file location and the wrapper key (`mcpServers`, except VS Code's
|
|
5
|
+
* `servers` which also needs an explicit `type: "stdio"`). Paths mirror the
|
|
6
|
+
* README "Compatibility" table.
|
|
7
|
+
*/
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
export const CLIENT_IDS = ["claude-code", "claude-desktop", "cursor", "windsurf", "vscode"];
|
|
11
|
+
/** Resolve a client id to its config location + shape, or null if unknown. */
|
|
12
|
+
export function clientInfo(id, projectPath) {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
switch (id) {
|
|
15
|
+
case "claude-code":
|
|
16
|
+
return { id, label: "Claude Code", configPath: null, key: "mcpServers", needsType: false };
|
|
17
|
+
case "claude-desktop": {
|
|
18
|
+
let p;
|
|
19
|
+
if (process.platform === "darwin") {
|
|
20
|
+
p = path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
21
|
+
}
|
|
22
|
+
else if (process.platform === "win32") {
|
|
23
|
+
p = path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
p = path.join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
27
|
+
}
|
|
28
|
+
return { id, label: "Claude Desktop", configPath: p, key: "mcpServers", needsType: false };
|
|
29
|
+
}
|
|
30
|
+
case "cursor":
|
|
31
|
+
return { id, label: "Cursor", configPath: path.join(home, ".cursor", "mcp.json"), key: "mcpServers", needsType: false };
|
|
32
|
+
case "windsurf":
|
|
33
|
+
return { id, label: "Windsurf", configPath: path.join(home, ".codeium", "windsurf", "mcp_config.json"), key: "mcpServers", needsType: false };
|
|
34
|
+
case "vscode":
|
|
35
|
+
// Project-scoped, so it lands next to the project the user is initialising.
|
|
36
|
+
return { id, label: "VS Code", configPath: path.join(projectPath, ".vscode", "mcp.json"), key: "servers", needsType: true };
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
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) {
|
|
43
|
+
const env = { GODOT_PROJECT: projectPath };
|
|
44
|
+
if (godotBin && godotBin !== "godot")
|
|
45
|
+
env.GODOT_BIN = godotBin;
|
|
46
|
+
const base = { command: "npx", args: ["-y", "breakpoint-mcp"], env };
|
|
47
|
+
return needsType ? { type: "stdio", ...base } : base;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Merge the server entry into an existing config's `key` object, preserving every
|
|
51
|
+
* other server. Returns pretty JSON. Throws if `existing` is present but not valid
|
|
52
|
+
* JSON, so the caller can refuse to clobber a file it can't parse.
|
|
53
|
+
*/
|
|
54
|
+
export function mergeClientConfig(existing, key, serverName, entry) {
|
|
55
|
+
let obj = {};
|
|
56
|
+
if (existing && existing.trim()) {
|
|
57
|
+
const parsed = JSON.parse(existing);
|
|
58
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
59
|
+
obj = parsed;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const existingSect = obj[key];
|
|
63
|
+
const sect = existingSect && typeof existingSect === "object" && !Array.isArray(existingSect)
|
|
64
|
+
? existingSect
|
|
65
|
+
: {};
|
|
66
|
+
sect[serverName] = entry;
|
|
67
|
+
obj[key] = sect;
|
|
68
|
+
return JSON.stringify(obj, null, 2) + "\n";
|
|
69
|
+
}
|
|
70
|
+
/** A copy-pasteable single-server snippet for the "print, don't write" default. */
|
|
71
|
+
export function snippet(key, serverName, entry) {
|
|
72
|
+
return JSON.stringify({ [key]: { [serverName]: entry } }, null, 2);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=clients.js.map
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `breakpoint-mcp doctor` — a verifiable health check for a Breakpoint MCP
|
|
3
|
+
* install, the CLI-side analogue of the planned in-editor status dock.
|
|
4
|
+
*
|
|
5
|
+
* It reports, for the configured project + environment:
|
|
6
|
+
* - the Godot binary (GODOT_BIN) runs and its version [required]
|
|
7
|
+
* - the editor addon is installed + enabled in project.godot [required, if a project is present]
|
|
8
|
+
* - the four bridges are reachable: editor 9080, runtime 9081,
|
|
9
|
+
* GDScript LSP 6005, GDScript DAP 6006 [info; --require-live promotes to required]
|
|
10
|
+
* - optionally, OmniSharp / netcoredbg are on PATH (C# planes) [info; --include-csharp]
|
|
11
|
+
*
|
|
12
|
+
* The exit code is 0 iff no *required* check failed, so `doctor` doubles as a
|
|
13
|
+
* pre-flight gate. Bridges default to informational because the editor/game
|
|
14
|
+
* may legitimately not be running when a user checks their install; pass
|
|
15
|
+
* `--require-live` when you expect them up (e.g. after opening the editor).
|
|
16
|
+
*
|
|
17
|
+
* Configuration is read via loadConfig(), so the same env overrides the server
|
|
18
|
+
* honours (GODOT_BIN, GODOT_PROJECT, and the BREAKPOINT_ / GODOT_ ports) apply here.
|
|
19
|
+
*/
|
|
20
|
+
import net from "node:net";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { loadConfig } from "../config.js";
|
|
25
|
+
import { parseArgs } from "./args.js";
|
|
26
|
+
const ADDON_REL = "addons/breakpoint_mcp";
|
|
27
|
+
const PLUGIN_CFG_RES = "res://addons/breakpoint_mcp/plugin.cfg";
|
|
28
|
+
/** Resolve after a TCP connect succeeds (true) or the port is closed/times out (false). */
|
|
29
|
+
function probeTcp(host, port, timeoutMs) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const socket = net.connect({ host, port });
|
|
32
|
+
let settled = false;
|
|
33
|
+
const done = (ok) => {
|
|
34
|
+
if (settled)
|
|
35
|
+
return;
|
|
36
|
+
settled = true;
|
|
37
|
+
socket.destroy();
|
|
38
|
+
resolve(ok);
|
|
39
|
+
};
|
|
40
|
+
socket.setTimeout(timeoutMs);
|
|
41
|
+
socket.once("connect", () => done(true));
|
|
42
|
+
socket.once("timeout", () => done(false));
|
|
43
|
+
socket.once("error", () => done(false));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function readText(p) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readFileSync(p, "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Is the Breakpoint MCP plugin listed in project.godot's [editor_plugins] enabled array? */
|
|
55
|
+
export function isPluginEnabled(projectGodotText) {
|
|
56
|
+
const lines = projectGodotText.split(/\r?\n/);
|
|
57
|
+
let inPlugins = false;
|
|
58
|
+
for (const raw of lines) {
|
|
59
|
+
const s = raw.trim();
|
|
60
|
+
if (s.startsWith("[") && s.endsWith("]")) {
|
|
61
|
+
inPlugins = s === "[editor_plugins]";
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (inPlugins && s.startsWith("enabled") && s.includes(PLUGIN_CFG_RES))
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function checkGodotBinary(bin, timeoutMs) {
|
|
70
|
+
const res = spawnSync(bin, ["--version"], { timeout: timeoutMs, encoding: "utf8" });
|
|
71
|
+
if (res.error) {
|
|
72
|
+
const code = res.error.code ?? res.error.message;
|
|
73
|
+
return {
|
|
74
|
+
name: "godot-binary",
|
|
75
|
+
status: "fail",
|
|
76
|
+
severity: "required",
|
|
77
|
+
detail: `'${bin}' is not runnable (${code})`,
|
|
78
|
+
hint: "Install Godot 4.2+ and put it on PATH, or set GODOT_BIN to its absolute path.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const version = (res.stdout ?? "").trim().split(/\r?\n/)[0] || "(no version output)";
|
|
82
|
+
return {
|
|
83
|
+
name: "godot-binary",
|
|
84
|
+
status: "ok",
|
|
85
|
+
severity: "required",
|
|
86
|
+
detail: `${bin} → ${version}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function checkAddon(projectPath) {
|
|
90
|
+
const projText = readText(path.join(projectPath, "project.godot"));
|
|
91
|
+
if (projText === null) {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
name: "project",
|
|
95
|
+
status: "skip",
|
|
96
|
+
severity: "info",
|
|
97
|
+
detail: `no project.godot at ${projectPath}`,
|
|
98
|
+
hint: "Pass --project <dir> pointing at your Godot project, or run from the project root.",
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
const checks = [];
|
|
103
|
+
const cfgText = readText(path.join(projectPath, ADDON_REL, "plugin.cfg"));
|
|
104
|
+
if (cfgText === null) {
|
|
105
|
+
checks.push({
|
|
106
|
+
name: "addon-installed",
|
|
107
|
+
status: "fail",
|
|
108
|
+
severity: "required",
|
|
109
|
+
detail: `not found at ${ADDON_REL}/plugin.cfg`,
|
|
110
|
+
hint: "Run 'breakpoint-mcp init' to install the editor addon into this project.",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const m = /version\s*=\s*"([^"]*)"/.exec(cfgText);
|
|
115
|
+
checks.push({
|
|
116
|
+
name: "addon-installed",
|
|
117
|
+
status: "ok",
|
|
118
|
+
severity: "required",
|
|
119
|
+
detail: `${ADDON_REL} (version ${m ? m[1] : "?"})`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
checks.push(isPluginEnabled(projText)
|
|
123
|
+
? {
|
|
124
|
+
name: "addon-enabled",
|
|
125
|
+
status: "ok",
|
|
126
|
+
severity: "required",
|
|
127
|
+
detail: "enabled in project.godot",
|
|
128
|
+
}
|
|
129
|
+
: {
|
|
130
|
+
name: "addon-enabled",
|
|
131
|
+
status: "fail",
|
|
132
|
+
severity: "required",
|
|
133
|
+
detail: "not listed under [editor_plugins] enabled",
|
|
134
|
+
hint: "Enable 'Breakpoint MCP' under Project → Project Settings → Plugins (or run 'breakpoint-mcp init').",
|
|
135
|
+
});
|
|
136
|
+
return checks;
|
|
137
|
+
}
|
|
138
|
+
/** Locate an executable on PATH without launching it (used for the C# info checks). */
|
|
139
|
+
function whichSync(cmd) {
|
|
140
|
+
if (path.isAbsolute(cmd)) {
|
|
141
|
+
try {
|
|
142
|
+
fs.accessSync(cmd, fs.constants.X_OK);
|
|
143
|
+
return cmd;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const dirs = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
150
|
+
const exts = process.platform === "win32"
|
|
151
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").map((e) => e.toLowerCase())
|
|
152
|
+
: [""];
|
|
153
|
+
for (const dir of dirs) {
|
|
154
|
+
for (const ext of exts) {
|
|
155
|
+
const full = path.join(dir, cmd + ext);
|
|
156
|
+
try {
|
|
157
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
158
|
+
return full;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
/* keep scanning */
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function checkCsharpTool(name, cmd) {
|
|
168
|
+
const found = whichSync(cmd);
|
|
169
|
+
return found
|
|
170
|
+
? { name, status: "ok", severity: "info", detail: `${cmd} → ${found}` }
|
|
171
|
+
: {
|
|
172
|
+
name,
|
|
173
|
+
status: "skip",
|
|
174
|
+
severity: "info",
|
|
175
|
+
detail: `${cmd} not on PATH (C# plane inactive until installed)`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export async function runDoctorChecks(config, opts) {
|
|
179
|
+
const checks = [];
|
|
180
|
+
// Godot binary (give the version probe a floor so a slow cold start isn't a false negative).
|
|
181
|
+
checks.push(checkGodotBinary(config.godotBin, Math.max(opts.timeoutMs, 3000)));
|
|
182
|
+
// Editor addon install + enable.
|
|
183
|
+
checks.push(...checkAddon(config.projectPath));
|
|
184
|
+
// The four bridges.
|
|
185
|
+
const severity = opts.requireLive ? "required" : "info";
|
|
186
|
+
const bridges = [
|
|
187
|
+
{
|
|
188
|
+
name: "editor-bridge",
|
|
189
|
+
host: config.bridgeHost,
|
|
190
|
+
port: config.bridgePort,
|
|
191
|
+
hint: 'Open the editor with the "Breakpoint MCP" plugin enabled.',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "runtime-bridge",
|
|
195
|
+
host: config.runtimeHost,
|
|
196
|
+
port: config.runtimePort,
|
|
197
|
+
hint: "Launch the project (godot_run_project / dbg_launch) with the plugin enabled — it auto-registers the runtime autoload.",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "gdscript-lsp",
|
|
201
|
+
host: config.lspHost,
|
|
202
|
+
port: config.lspPort,
|
|
203
|
+
hint: "Godot's language server runs while the editor is open (Editor → Editor Settings → Network → Language Server).",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "gdscript-dap",
|
|
207
|
+
host: config.dapHost,
|
|
208
|
+
port: config.dapPort,
|
|
209
|
+
hint: "Godot's debug adapter runs while the editor is open (Editor → Editor Settings → Network → Debug Adapter).",
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
const bridgeChecks = await Promise.all(bridges.map(async (b) => {
|
|
213
|
+
const ok = await probeTcp(b.host, b.port, opts.timeoutMs);
|
|
214
|
+
return {
|
|
215
|
+
name: b.name,
|
|
216
|
+
status: ok ? "ok" : "fail",
|
|
217
|
+
severity,
|
|
218
|
+
detail: `${b.host}:${b.port} ${ok ? "reachable" : "unreachable"}`,
|
|
219
|
+
hint: ok ? undefined : b.hint,
|
|
220
|
+
};
|
|
221
|
+
}));
|
|
222
|
+
checks.push(...bridgeChecks);
|
|
223
|
+
// Optional C# tooling.
|
|
224
|
+
if (opts.includeCsharp) {
|
|
225
|
+
checks.push(checkCsharpTool("csharp-lsp", config.csLspCmd));
|
|
226
|
+
checks.push(checkCsharpTool("csharp-dap", config.csDapCmd));
|
|
227
|
+
}
|
|
228
|
+
const ok = checks.every((c) => c.severity !== "required" || c.status !== "fail");
|
|
229
|
+
return { checks, ok };
|
|
230
|
+
}
|
|
231
|
+
function glyph(status) {
|
|
232
|
+
return status === "ok" ? "✓" : status === "fail" ? "✗" : "–";
|
|
233
|
+
}
|
|
234
|
+
function printReport(report) {
|
|
235
|
+
const width = Math.max(...report.checks.map((c) => c.name.length));
|
|
236
|
+
const out = ["breakpoint-mcp doctor", ""];
|
|
237
|
+
for (const c of report.checks) {
|
|
238
|
+
out.push(` ${glyph(c.status)} ${c.name.padEnd(width)} ${c.detail}`);
|
|
239
|
+
if (c.status === "fail" && c.hint)
|
|
240
|
+
out.push(` ↳ ${c.hint}`);
|
|
241
|
+
}
|
|
242
|
+
out.push("");
|
|
243
|
+
out.push(report.ok
|
|
244
|
+
? "All required checks passed."
|
|
245
|
+
: "Some required checks failed — see the ↳ hints above.");
|
|
246
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
247
|
+
}
|
|
248
|
+
/** Entry point for `breakpoint-mcp doctor`. Returns the process exit code. */
|
|
249
|
+
export async function runDoctor(argv) {
|
|
250
|
+
const { flags } = parseArgs(argv, ["json", "require-live", "include-csharp"]);
|
|
251
|
+
if (typeof flags.project === "string")
|
|
252
|
+
process.env.GODOT_PROJECT = flags.project;
|
|
253
|
+
const timeoutRaw = typeof flags.timeout === "string" ? Number.parseInt(flags.timeout, 10) : NaN;
|
|
254
|
+
const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : 1500;
|
|
255
|
+
const config = loadConfig();
|
|
256
|
+
const report = await runDoctorChecks(config, {
|
|
257
|
+
timeoutMs,
|
|
258
|
+
requireLive: flags["require-live"] === true,
|
|
259
|
+
includeCsharp: flags["include-csharp"] === true,
|
|
260
|
+
});
|
|
261
|
+
if (flags.json === true) {
|
|
262
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
printReport(report);
|
|
266
|
+
}
|
|
267
|
+
return report.ok ? 0 : 1;
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=doctor.js.map
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `breakpoint-mcp init` — one-command onboarding. Installs the editor addon into
|
|
3
|
+
* a target Godot project, enables it in `project.godot`, and wires (or prints)
|
|
4
|
+
* the MCP-client config. Idempotent and non-destructive: an existing addon is
|
|
5
|
+
* skipped unless `--force`, an already-enabled plugin is a no-op, and a client
|
|
6
|
+
* config is backed up (`.bak`) before it is merged. `--dry-run` touches nothing.
|
|
7
|
+
*
|
|
8
|
+
* By default the client snippet is printed; pass `--client <id>` to write/merge
|
|
9
|
+
* it into that client's config file (see clients.ts for the id list + paths).
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { parseArgs } from "./args.js";
|
|
15
|
+
import { CLIENT_IDS, clientInfo, mergeClientConfig, serverEntry, snippet } from "./clients.js";
|
|
16
|
+
const PLUGIN_REL = "addons/breakpoint_mcp";
|
|
17
|
+
const PLUGIN_CFG_RES = "res://addons/breakpoint_mcp/plugin.cfg";
|
|
18
|
+
const SERVER_NAME = "godot";
|
|
19
|
+
/**
|
|
20
|
+
* Locate the addon shipped with this package. In the published tarball it lives
|
|
21
|
+
* at <pkg>/addon/breakpoint_mcp (staged there by scripts/stage-addon.mjs); in the
|
|
22
|
+
* dev tree the package root is host/, so the canonical repo-root addons/ copy is
|
|
23
|
+
* one level up. First match with a plugin.cfg wins.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveBundledAddon() {
|
|
26
|
+
// Escape hatch / test hook: an explicit addon source dir wins when it has a plugin.cfg.
|
|
27
|
+
const override = process.env.BREAKPOINT_ADDON_SRC;
|
|
28
|
+
if (override && fs.existsSync(path.join(override, "plugin.cfg")))
|
|
29
|
+
return override;
|
|
30
|
+
const here = path.dirname(fileURLToPath(import.meta.url)); // dist/cli
|
|
31
|
+
const pkgRoot = path.join(here, "..", ".."); // dist/cli -> dist -> package root
|
|
32
|
+
const candidates = [
|
|
33
|
+
path.join(pkgRoot, "addon", "breakpoint_mcp"), // published tarball
|
|
34
|
+
path.join(pkgRoot, "..", "addons", "breakpoint_mcp"), // dev tree (host/../addons)
|
|
35
|
+
];
|
|
36
|
+
for (const c of candidates) {
|
|
37
|
+
if (fs.existsSync(path.join(c, "plugin.cfg")))
|
|
38
|
+
return c;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Add `res://addons/breakpoint_mcp/plugin.cfg` to project.godot's
|
|
44
|
+
* `[editor_plugins] enabled=PackedStringArray(...)`, creating the section or the
|
|
45
|
+
* line if absent and preserving every other plugin. Pure — returns the new text.
|
|
46
|
+
*/
|
|
47
|
+
export function enablePlugin(projectGodotText) {
|
|
48
|
+
const RES = PLUGIN_CFG_RES;
|
|
49
|
+
const text = projectGodotText;
|
|
50
|
+
const lines = text.split("\n");
|
|
51
|
+
let sectionIdx = -1;
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
if (lines[i].trim() === "[editor_plugins]") {
|
|
54
|
+
sectionIdx = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (sectionIdx === -1) {
|
|
59
|
+
const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
|
|
60
|
+
const block = `${sep}\n[editor_plugins]\n\nenabled=PackedStringArray("${RES}")\n`;
|
|
61
|
+
return { text: text + block, changed: true, alreadyEnabled: false };
|
|
62
|
+
}
|
|
63
|
+
let enabledIdx = -1;
|
|
64
|
+
for (let i = sectionIdx + 1; i < lines.length; i++) {
|
|
65
|
+
const s = lines[i].trim();
|
|
66
|
+
if (s.startsWith("[") && s.endsWith("]"))
|
|
67
|
+
break; // next section
|
|
68
|
+
if (s.startsWith("enabled")) {
|
|
69
|
+
enabledIdx = i;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (enabledIdx === -1) {
|
|
74
|
+
lines.splice(sectionIdx + 1, 0, `enabled=PackedStringArray("${RES}")`);
|
|
75
|
+
return { text: lines.join("\n"), changed: true, alreadyEnabled: false };
|
|
76
|
+
}
|
|
77
|
+
const line = lines[enabledIdx];
|
|
78
|
+
if (line.includes(RES)) {
|
|
79
|
+
return { text, changed: false, alreadyEnabled: true };
|
|
80
|
+
}
|
|
81
|
+
const m = /PackedStringArray\(([^)]*)\)/.exec(line);
|
|
82
|
+
if (!m) {
|
|
83
|
+
lines.splice(enabledIdx + 1, 0, `enabled=PackedStringArray("${RES}")`);
|
|
84
|
+
return { text: lines.join("\n"), changed: true, alreadyEnabled: false };
|
|
85
|
+
}
|
|
86
|
+
const inside = m[1].trim();
|
|
87
|
+
const newInside = inside.length === 0 ? `"${RES}"` : `${inside}, "${RES}"`;
|
|
88
|
+
lines[enabledIdx] = line.replace(/PackedStringArray\([^)]*\)/, `PackedStringArray(${newInside})`);
|
|
89
|
+
return { text: lines.join("\n"), changed: true, alreadyEnabled: false };
|
|
90
|
+
}
|
|
91
|
+
/** Copy the addon into <project>/addons/breakpoint_mcp; skip if present unless force. */
|
|
92
|
+
export function installAddon(addonSource, projectPath, opts) {
|
|
93
|
+
const dest = path.join(projectPath, PLUGIN_REL);
|
|
94
|
+
const exists = fs.existsSync(path.join(dest, "plugin.cfg"));
|
|
95
|
+
if (exists && !opts.force)
|
|
96
|
+
return { action: "skipped", dest };
|
|
97
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
98
|
+
fs.cpSync(addonSource, dest, { recursive: true });
|
|
99
|
+
return { action: exists ? "overwritten" : "installed", dest };
|
|
100
|
+
}
|
|
101
|
+
function claudeCodeCommand(projectPath) {
|
|
102
|
+
return `claude mcp add godot --env GODOT_PROJECT=${projectPath} -- npx -y breakpoint-mcp`;
|
|
103
|
+
}
|
|
104
|
+
/** Entry point for `breakpoint-mcp init`. Returns the process exit code. */
|
|
105
|
+
export async function runInit(argv) {
|
|
106
|
+
const { flags } = parseArgs(argv, ["force", "dry-run"]);
|
|
107
|
+
const projectPath = typeof flags.project === "string"
|
|
108
|
+
? path.resolve(flags.project)
|
|
109
|
+
: process.env.GODOT_PROJECT ?? process.cwd();
|
|
110
|
+
const dryRun = flags["dry-run"] === true;
|
|
111
|
+
const force = flags.force === true;
|
|
112
|
+
const client = typeof flags.client === "string" ? flags.client : "none";
|
|
113
|
+
const godotBin = process.env.GODOT_BIN ?? "godot";
|
|
114
|
+
const projGodot = path.join(projectPath, "project.godot");
|
|
115
|
+
if (!fs.existsSync(projGodot)) {
|
|
116
|
+
process.stderr.write(`init: no project.godot at ${projectPath}\n` +
|
|
117
|
+
" Pass --project <dir> pointing at your Godot project (the folder with project.godot).\n");
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
const addonSource = resolveBundledAddon();
|
|
121
|
+
if (!addonSource) {
|
|
122
|
+
process.stderr.write("init: could not locate the bundled editor addon inside the package.\n");
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
const out = [];
|
|
126
|
+
const say = (s = "") => {
|
|
127
|
+
out.push(s);
|
|
128
|
+
};
|
|
129
|
+
say(`Project: ${projectPath}${dryRun ? " (dry run — no changes written)" : ""}`);
|
|
130
|
+
// 1. Install the addon.
|
|
131
|
+
const destHasAddon = fs.existsSync(path.join(projectPath, PLUGIN_REL, "plugin.cfg"));
|
|
132
|
+
if (dryRun) {
|
|
133
|
+
const verb = destHasAddon ? (force ? "overwrite" : "skip (already present; --force to overwrite)") : "install";
|
|
134
|
+
say(` addon: would ${verb} → ${PLUGIN_REL}/`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const r = installAddon(addonSource, projectPath, { force });
|
|
138
|
+
say(` addon: ${r.action} → ${PLUGIN_REL}/`);
|
|
139
|
+
}
|
|
140
|
+
// 2. Enable it in project.godot.
|
|
141
|
+
const before = fs.readFileSync(projGodot, "utf8");
|
|
142
|
+
const en = enablePlugin(before);
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
say(` plugin: would ${en.alreadyEnabled ? "leave enabled (already enabled)" : "enable in project.godot"}`);
|
|
145
|
+
}
|
|
146
|
+
else if (en.changed) {
|
|
147
|
+
fs.writeFileSync(projGodot, en.text);
|
|
148
|
+
say(" plugin: enabled in project.godot");
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
say(" plugin: already enabled");
|
|
152
|
+
}
|
|
153
|
+
// 3. MCP-client config.
|
|
154
|
+
say("");
|
|
155
|
+
if (client === "claude-code") {
|
|
156
|
+
say("MCP client (Claude Code) — run:");
|
|
157
|
+
say(` ${claudeCodeCommand(projectPath)}`);
|
|
158
|
+
}
|
|
159
|
+
else if (client === "none") {
|
|
160
|
+
say('MCP client — add this to your client config (wrapper key "mcpServers"):');
|
|
161
|
+
say(snippet("mcpServers", SERVER_NAME, serverEntry(projectPath, godotBin, false)));
|
|
162
|
+
say("");
|
|
163
|
+
say("Claude Code users can instead run:");
|
|
164
|
+
say(` ${claudeCodeCommand(projectPath)}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const info = clientInfo(client, projectPath);
|
|
168
|
+
if (!info || info.configPath === null) {
|
|
169
|
+
process.stderr.write(`init: unknown --client '${client}'. Known: ${CLIENT_IDS.join(", ")}.\n`);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const entry = serverEntry(projectPath, godotBin, info.needsType);
|
|
173
|
+
if (dryRun) {
|
|
174
|
+
say(`MCP client (${info.label}): would write ${info.configPath}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const existed = fs.existsSync(info.configPath);
|
|
178
|
+
const prev = existed ? fs.readFileSync(info.configPath, "utf8") : null;
|
|
179
|
+
let merged;
|
|
180
|
+
try {
|
|
181
|
+
merged = mergeClientConfig(prev, info.key, SERVER_NAME, entry);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
process.stderr.write(`init: ${info.configPath} is not valid JSON — leaving it untouched.\n`);
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
fs.mkdirSync(path.dirname(info.configPath), { recursive: true });
|
|
188
|
+
if (existed)
|
|
189
|
+
fs.copyFileSync(info.configPath, info.configPath + ".bak");
|
|
190
|
+
fs.writeFileSync(info.configPath, merged);
|
|
191
|
+
say(`MCP client (${info.label}): ${existed ? "updated" : "created"} ${info.configPath}${existed ? " (backup at .bak)" : ""}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
say("");
|
|
195
|
+
say("Next: open the project in Godot, then run `breakpoint-mcp doctor --require-live` to verify.");
|
|
196
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=init.js.map
|
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:
|
|
36
|
-
bridgePort: Number.parseInt(
|
|
37
|
-
bridgeTimeoutMs: Number.parseInt(
|
|
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:
|
|
62
|
-
runtimePort: Number.parseInt(
|
|
63
|
-
runtimeTimeoutMs: Number.parseInt(
|
|
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 {
|
|
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.
|
|
46
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.2.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 =
|
|
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();
|
|
@@ -103,7 +103,52 @@ async function main() {
|
|
|
103
103
|
process.on("SIGINT", shutdown);
|
|
104
104
|
process.on("SIGTERM", shutdown);
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
function printUsage() {
|
|
107
|
+
process.stdout.write([
|
|
108
|
+
"breakpoint-mcp — MCP server exposing Godot to AI coding assistants.",
|
|
109
|
+
"",
|
|
110
|
+
"Usage:",
|
|
111
|
+
" breakpoint-mcp Start the MCP server on stdio (default; how MCP clients launch it).",
|
|
112
|
+
" breakpoint-mcp init Install + enable the editor addon in a project and wire the MCP client.",
|
|
113
|
+
" breakpoint-mcp doctor Check the Godot binary, the editor addon, and the four bridges.",
|
|
114
|
+
" breakpoint-mcp --help Show this help.",
|
|
115
|
+
"",
|
|
116
|
+
"init options:",
|
|
117
|
+
" --project <dir> Target Godot project (default: $GODOT_PROJECT or the current directory).",
|
|
118
|
+
" --client <id> Write the MCP config for a client: claude-code | claude-desktop | cursor | windsurf | vscode.",
|
|
119
|
+
" --force Overwrite an addon that is already installed.",
|
|
120
|
+
" --dry-run Print what would change without writing anything.",
|
|
121
|
+
"",
|
|
122
|
+
"doctor options:",
|
|
123
|
+
" --project <dir> Project to check (default: $GODOT_PROJECT or the current directory).",
|
|
124
|
+
" --require-live Also require the editor/runtime/LSP/DAP bridges to be reachable.",
|
|
125
|
+
" --include-csharp Also probe OmniSharp / netcoredbg on PATH (the C# planes).",
|
|
126
|
+
" --timeout <ms> Per-bridge connect timeout (default 1500).",
|
|
127
|
+
" --json Emit the report as JSON.",
|
|
128
|
+
"",
|
|
129
|
+
"All runtime configuration is via environment variables; see the README.",
|
|
130
|
+
"",
|
|
131
|
+
].join("\n"));
|
|
132
|
+
}
|
|
133
|
+
// Subcommand dispatch. Anything that isn't a recognized subcommand — including
|
|
134
|
+
// no arguments at all, which is how every MCP client launches this — falls
|
|
135
|
+
// through to the stdio server, so the server's launch contract is unchanged.
|
|
136
|
+
void (async () => {
|
|
137
|
+
const sub = process.argv[2];
|
|
138
|
+
if (sub === "doctor") {
|
|
139
|
+
const { runDoctor } = await import("./cli/doctor.js");
|
|
140
|
+
process.exit(await runDoctor(process.argv.slice(3)));
|
|
141
|
+
}
|
|
142
|
+
if (sub === "init") {
|
|
143
|
+
const { runInit } = await import("./cli/init.js");
|
|
144
|
+
process.exit(await runInit(process.argv.slice(3)));
|
|
145
|
+
}
|
|
146
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
147
|
+
printUsage();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
await main();
|
|
151
|
+
})().catch((err) => {
|
|
107
152
|
log("fatal:", err instanceof Error ? err.stack ?? err.message : String(err));
|
|
108
153
|
process.exit(1);
|
|
109
154
|
});
|