breakpoint-mcp 1.4.0 → 1.12.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 +4 -2
- package/addon/breakpoint_mcp/LICENSE +21 -0
- package/addon/breakpoint_mcp/bridge_server.gd +16 -0
- package/addon/breakpoint_mcp/operations.gd +1 -1
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/dist/cli/github.js +97 -0
- package/dist/cli/init.js +65 -7
- package/dist/index.js +15 -1
- package/dist/schemas.js +215 -0
- package/dist/tools/tabletop.js +1775 -0
- package/dist/tools/vcs.js +458 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The MCP **host** for [Breakpoint MCP](https://github.com/jlivingston-Cipher/godot-breakpoint-mcp) —
|
|
4
4
|
a [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
|
|
5
5
|
Godot game engine to AI coding assistants across four planes: headless CLI, the live
|
|
6
|
-
editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **
|
|
6
|
+
editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **258 tools
|
|
7
7
|
+ 5 MCP resources**, built against the stable `@modelcontextprotocol/sdk` 1.x API.
|
|
8
8
|
Developed and tested with **Claude**; because MCP is an open protocol, other clients can
|
|
9
9
|
connect too (currently untested — reports welcome).
|
|
@@ -30,7 +30,9 @@ npx breakpoint-mcp doctor
|
|
|
30
30
|
|
|
31
31
|
`init` copies the addon into `addons/breakpoint_mcp/`, enables it in `project.godot`, and
|
|
32
32
|
prints — or, with `--client claude-code|claude-desktop|cursor|windsurf|vscode`, writes —
|
|
33
|
-
the client config.
|
|
33
|
+
the client config. By default it uses the addon bundled in this package; add
|
|
34
|
+
`--from-github [ref]` to fetch the addon from GitHub instead (e.g. `--from-github main`).
|
|
35
|
+
`doctor` checks the Godot binary, the addon, and the four bridges
|
|
34
36
|
(add `--require-live` once the editor is open; `--json` for a machine-readable report).
|
|
35
37
|
|
|
36
38
|
To install the `breakpoint-mcp` command globally instead of invoking it via `npx`:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 James Livingston and breakpoint-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -18,6 +18,11 @@ var _server: TCPServer
|
|
|
18
18
|
var _ops: Operations
|
|
19
19
|
var _clients: Array = [] # Array of {peer: StreamPeerTCP, buf: String}
|
|
20
20
|
var _port: int = DEFAULT_PORT
|
|
21
|
+
## Re-entrancy guard for `_process` (Finding D). True while a request dispatch is
|
|
22
|
+
## on the stack, so a handler that pumps the editor main loop (e.g. `scene.save`
|
|
23
|
+
## triggers a filesystem rescan/reimport) cannot re-enter `_process` and re-drain
|
|
24
|
+
## the same still-buffered line, which would recurse until the stack overflows.
|
|
25
|
+
var _dispatching: bool = false
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
func setup(plugin: EditorPlugin) -> void:
|
|
@@ -64,6 +69,16 @@ func get_status() -> Dictionary:
|
|
|
64
69
|
func _process(_delta: float) -> void:
|
|
65
70
|
if _server == null:
|
|
66
71
|
return
|
|
72
|
+
# Finding D: a request handler can pump the editor main loop (e.g. scene.save
|
|
73
|
+
# runs a filesystem rescan/reimport), which makes the engine call _process
|
|
74
|
+
# again *inside* the current dispatch. A client's line is only cleared from
|
|
75
|
+
# c["buf"] after _drain_lines returns, so a re-entrant tick would re-read and
|
|
76
|
+
# re-dispatch the SAME line, recursing until the stack overflows
|
|
77
|
+
# (operations.gd:512 _scene_save <-> _drain_lines/_handle_line here). Skip
|
|
78
|
+
# re-entrant ticks; buffered bytes are serviced on the next top-level tick.
|
|
79
|
+
if _dispatching:
|
|
80
|
+
return
|
|
81
|
+
_dispatching = true
|
|
67
82
|
# Accept new connections.
|
|
68
83
|
while _server.is_connection_available():
|
|
69
84
|
var peer := _server.take_connection()
|
|
@@ -86,6 +101,7 @@ func _process(_delta: float) -> void:
|
|
|
86
101
|
_drain_lines(c)
|
|
87
102
|
still_alive.append(c)
|
|
88
103
|
_clients = still_alive
|
|
104
|
+
_dispatching = false
|
|
89
105
|
|
|
90
106
|
|
|
91
107
|
func _drain_lines(c: Dictionary) -> void:
|
|
@@ -7,7 +7,7 @@ extends RefCounted
|
|
|
7
7
|
## wrapped in the EditorUndoRedoManager so a human can Ctrl-Z anything the assistant did.
|
|
8
8
|
|
|
9
9
|
const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
|
|
10
|
-
const ADDON_VERSION := "1.4.
|
|
10
|
+
const ADDON_VERSION := "1.4.2"
|
|
11
11
|
|
|
12
12
|
var _plugin: EditorPlugin
|
|
13
13
|
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
name="Breakpoint MCP"
|
|
4
4
|
description="Exposes the live Godot editor and the running game to an MCP host so an AI assistant can drive them: scene/node/resource CRUD with full undo/redo, project settings, ClassDB, editor + in-game screenshots, and an in-game runtime bridge (live SceneTree, property get/set, method calls, signals, input injection, Performance monitors). Pairs with the breakpoint-mcp MCP host."
|
|
5
5
|
author="James Livingston"
|
|
6
|
-
version="1.4.
|
|
6
|
+
version="1.4.2"
|
|
7
7
|
script="plugin.gd"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--from-github` escape hatch for `breakpoint-mcp init`: fetch the editor addon
|
|
3
|
+
* (addons/breakpoint_mcp/**) straight from the GitHub repo at a chosen ref, as an
|
|
4
|
+
* alternative to the copy bundled in the npm tarball. Used when the bundled addon
|
|
5
|
+
* is missing/corrupt, or to install a different ref (e.g. `main`, or an older tag)
|
|
6
|
+
* than the one that shipped with the installed package.
|
|
7
|
+
*
|
|
8
|
+
* Dependency-free: one GitHub git/trees API call lists the addon's blobs at the ref,
|
|
9
|
+
* then each file is downloaded from raw.githubusercontent.com (a CDN that does not
|
|
10
|
+
* count against the REST rate limit). The fetch-shaped dependency is injectable, so
|
|
11
|
+
* the whole path is unit-testable offline.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
export const DEFAULT_REPO = "jlivingston-Cipher/godot-breakpoint-mcp";
|
|
16
|
+
const ADDON_PREFIX = "addons/breakpoint_mcp/";
|
|
17
|
+
const USER_AGENT = "breakpoint-mcp-cli";
|
|
18
|
+
function defaultFetch() {
|
|
19
|
+
const f = globalThis.fetch;
|
|
20
|
+
if (typeof f !== "function") {
|
|
21
|
+
throw new Error("global fetch is unavailable — Node 18+ is required for --from-github.");
|
|
22
|
+
}
|
|
23
|
+
return f;
|
|
24
|
+
}
|
|
25
|
+
/** Validate and split an `owner/repo` slug; throws on anything else. */
|
|
26
|
+
export function parseRepo(repo) {
|
|
27
|
+
const m = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(repo.trim());
|
|
28
|
+
if (!m)
|
|
29
|
+
throw new Error(`invalid --repo '${repo}' (expected owner/repo).`);
|
|
30
|
+
return { owner: m[1], name: m[2] };
|
|
31
|
+
}
|
|
32
|
+
function authHeaders() {
|
|
33
|
+
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
34
|
+
const h = { "User-Agent": USER_AGENT };
|
|
35
|
+
if (token)
|
|
36
|
+
h.Authorization = `Bearer ${token}`;
|
|
37
|
+
return h;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fetch addons/breakpoint_mcp/** from `repo` at `ref` into `dest` (created if
|
|
41
|
+
* needed). Returns the written relative paths. Throws a clear Error on a bad
|
|
42
|
+
* repo/ref, a missing addon, rate-limiting, or a network failure.
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchAddonFromGitHub(opts, fetchFn) {
|
|
45
|
+
const doFetch = fetchFn ?? defaultFetch();
|
|
46
|
+
const { owner, name } = parseRepo(opts.repo);
|
|
47
|
+
const ref = opts.ref;
|
|
48
|
+
const treeUrl = `https://api.github.com/repos/${owner}/${name}/git/trees/${ref}?recursive=1`;
|
|
49
|
+
let treeRes;
|
|
50
|
+
try {
|
|
51
|
+
treeRes = await doFetch(treeUrl, {
|
|
52
|
+
headers: { ...authHeaders(), Accept: "application/vnd.github+json" },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw new Error(`could not reach GitHub: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
}
|
|
58
|
+
if (treeRes.status === 404) {
|
|
59
|
+
throw new Error(`GitHub returned 404 for ${owner}/${name}@${ref} — check the repo and ref.`);
|
|
60
|
+
}
|
|
61
|
+
if (treeRes.status === 403) {
|
|
62
|
+
throw new Error("GitHub returned 403 (rate-limited or forbidden). Set GITHUB_TOKEN to raise the limit, or retry later.");
|
|
63
|
+
}
|
|
64
|
+
if (!treeRes.ok) {
|
|
65
|
+
throw new Error(`GitHub git/trees request failed (HTTP ${treeRes.status}).`);
|
|
66
|
+
}
|
|
67
|
+
const body = (await treeRes.json());
|
|
68
|
+
if (body.truncated) {
|
|
69
|
+
throw new Error(`the repository tree at ${ref} was too large to list in one request; --from-github can't be used for this ref.`);
|
|
70
|
+
}
|
|
71
|
+
const entries = Array.isArray(body.tree) ? body.tree : [];
|
|
72
|
+
const files = entries.filter((e) => e.type === "blob" && e.path.startsWith(ADDON_PREFIX));
|
|
73
|
+
if (files.length === 0) {
|
|
74
|
+
throw new Error(`no ${ADDON_PREFIX} found in ${owner}/${name}@${ref}.`);
|
|
75
|
+
}
|
|
76
|
+
const written = [];
|
|
77
|
+
for (const f of files) {
|
|
78
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${name}/${ref}/${f.path}`;
|
|
79
|
+
let res;
|
|
80
|
+
try {
|
|
81
|
+
res = await doFetch(rawUrl, { headers: { "User-Agent": USER_AGENT } });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
throw new Error(`could not download ${f.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error(`could not download ${f.path} (HTTP ${res.status}).`);
|
|
88
|
+
}
|
|
89
|
+
const rel = f.path.slice(ADDON_PREFIX.length);
|
|
90
|
+
const outPath = path.join(opts.dest, rel);
|
|
91
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
92
|
+
fs.writeFileSync(outPath, Buffer.from(await res.arrayBuffer()));
|
|
93
|
+
written.push(rel);
|
|
94
|
+
}
|
|
95
|
+
return written;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=github.js.map
|
package/dist/cli/init.js
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* it into that client's config file (see clients.ts for the id list + paths).
|
|
10
10
|
*/
|
|
11
11
|
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { parseArgs } from "./args.js";
|
|
15
16
|
import { CLIENT_IDS, clientInfo, mergeClientConfig, serverEntry, snippet } from "./clients.js";
|
|
17
|
+
import { DEFAULT_REPO, fetchAddonFromGitHub } from "./github.js";
|
|
16
18
|
const PLUGIN_REL = "addons/breakpoint_mcp";
|
|
17
19
|
const PLUGIN_CFG_RES = "res://addons/breakpoint_mcp/plugin.cfg";
|
|
18
20
|
const SERVER_NAME = "godot";
|
|
@@ -39,6 +41,23 @@ export function resolveBundledAddon() {
|
|
|
39
41
|
}
|
|
40
42
|
return null;
|
|
41
43
|
}
|
|
44
|
+
/** This package's own version, read from its package.json (null if unreadable). */
|
|
45
|
+
function packageVersion() {
|
|
46
|
+
try {
|
|
47
|
+
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); // dist/cli -> pkg
|
|
48
|
+
const v = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"))
|
|
49
|
+
.version;
|
|
50
|
+
return typeof v === "string" ? v : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Default ref for --from-github: this package's version tag (vX.Y.Z), else main. */
|
|
57
|
+
function defaultGitHubRef() {
|
|
58
|
+
const v = packageVersion();
|
|
59
|
+
return v ? `v${v}` : "main";
|
|
60
|
+
}
|
|
42
61
|
/**
|
|
43
62
|
* Add `res://addons/breakpoint_mcp/plugin.cfg` to project.godot's
|
|
44
63
|
* `[editor_plugins] enabled=PackedStringArray(...)`, creating the section or the
|
|
@@ -102,7 +121,7 @@ function claudeCodeCommand(projectPath) {
|
|
|
102
121
|
return `claude mcp add godot --env GODOT_PROJECT=${projectPath} -- npx -y breakpoint-mcp`;
|
|
103
122
|
}
|
|
104
123
|
/** Entry point for `breakpoint-mcp init`. Returns the process exit code. */
|
|
105
|
-
export async function runInit(argv) {
|
|
124
|
+
export async function runInit(argv, deps = {}) {
|
|
106
125
|
const { flags } = parseArgs(argv, ["force", "dry-run"]);
|
|
107
126
|
const projectPath = typeof flags.project === "string"
|
|
108
127
|
? path.resolve(flags.project)
|
|
@@ -111,30 +130,69 @@ export async function runInit(argv) {
|
|
|
111
130
|
const force = flags.force === true;
|
|
112
131
|
const client = typeof flags.client === "string" ? flags.client : "none";
|
|
113
132
|
const godotBin = process.env.GODOT_BIN ?? "godot";
|
|
133
|
+
// --from-github [ref]: source the addon from GitHub instead of the bundled copy.
|
|
134
|
+
const fromGitHub = "from-github" in flags;
|
|
135
|
+
const fgVal = flags["from-github"];
|
|
136
|
+
const ref = typeof fgVal === "string" && fgVal.length > 0 ? fgVal : defaultGitHubRef();
|
|
137
|
+
const repo = typeof flags.repo === "string" && flags.repo.length > 0 ? flags.repo : DEFAULT_REPO;
|
|
114
138
|
const projGodot = path.join(projectPath, "project.godot");
|
|
115
139
|
if (!fs.existsSync(projGodot)) {
|
|
116
140
|
process.stderr.write(`init: no project.godot at ${projectPath}\n` +
|
|
117
141
|
" Pass --project <dir> pointing at your Godot project (the folder with project.godot).\n");
|
|
118
142
|
return 1;
|
|
119
143
|
}
|
|
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
144
|
const out = [];
|
|
126
145
|
const say = (s = "") => {
|
|
127
146
|
out.push(s);
|
|
128
147
|
};
|
|
129
148
|
say(`Project: ${projectPath}${dryRun ? " (dry run — no changes written)" : ""}`);
|
|
149
|
+
// Resolve the addon source: the bundled copy, or (--from-github) a temp dir
|
|
150
|
+
// fetched from GitHub. The temp dir is removed once the addon is installed.
|
|
151
|
+
let addonSource = null;
|
|
152
|
+
let tmpFetchDir = null;
|
|
153
|
+
if (fromGitHub) {
|
|
154
|
+
if (dryRun) {
|
|
155
|
+
say(` addon source: would fetch ${repo}@${ref} from GitHub`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
say(` addon source: fetching ${repo}@${ref} from GitHub…`);
|
|
159
|
+
tmpFetchDir = fs.mkdtempSync(path.join(os.tmpdir(), "bpmcp-gh-"));
|
|
160
|
+
try {
|
|
161
|
+
const written = await fetchAddonFromGitHub({ repo, ref, dest: tmpFetchDir }, deps.fetchFn);
|
|
162
|
+
say(` addon source: fetched ${written.length} file(s) from ${repo}@${ref}`);
|
|
163
|
+
addonSource = tmpFetchDir;
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
fs.rmSync(tmpFetchDir, { recursive: true, force: true });
|
|
167
|
+
process.stderr.write(`init: --from-github failed: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
168
|
+
" Omit --from-github to install the addon bundled with this package.\n");
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
addonSource = resolveBundledAddon();
|
|
175
|
+
if (!addonSource) {
|
|
176
|
+
process.stderr.write("init: could not locate the bundled editor addon inside the package.\n" +
|
|
177
|
+
" Reinstall breakpoint-mcp, or pass --from-github to fetch the addon from GitHub.\n");
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
130
181
|
// 1. Install the addon.
|
|
131
182
|
const destHasAddon = fs.existsSync(path.join(projectPath, PLUGIN_REL, "plugin.cfg"));
|
|
132
183
|
if (dryRun) {
|
|
133
184
|
const verb = destHasAddon ? (force ? "overwrite" : "skip (already present; --force to overwrite)") : "install";
|
|
134
|
-
|
|
185
|
+
const srcNote = fromGitHub ? ` (from ${repo}@${ref})` : "";
|
|
186
|
+
say(` addon: would ${verb} → ${PLUGIN_REL}/${srcNote}`);
|
|
135
187
|
}
|
|
136
188
|
else {
|
|
189
|
+
if (!addonSource) {
|
|
190
|
+
process.stderr.write("init: internal error — no addon source resolved.\n");
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
137
193
|
const r = installAddon(addonSource, projectPath, { force });
|
|
194
|
+
if (tmpFetchDir)
|
|
195
|
+
fs.rmSync(tmpFetchDir, { recursive: true, force: true });
|
|
138
196
|
say(` addon: ${r.action} → ${PLUGIN_REL}/`);
|
|
139
197
|
}
|
|
140
198
|
// 2. Enable it in project.godot.
|
package/dist/index.js
CHANGED
|
@@ -17,9 +17,11 @@ import { registerCsDapTools } from "./tools/csdap.js";
|
|
|
17
17
|
import { registerRuntimeTools } from "./tools/runtime.js";
|
|
18
18
|
import { registerProcessTools } from "./tools/processes.js";
|
|
19
19
|
import { registerKnowledgeTools } from "./tools/knowledge.js";
|
|
20
|
+
import { registerVcsTools } from "./tools/vcs.js";
|
|
20
21
|
import { registerAssetGenTools } from "./tools/assetgen.js";
|
|
21
22
|
import { registerNetcodeTools } from "./tools/netcode.js";
|
|
22
23
|
import { registerBackendTools } from "./tools/backend.js";
|
|
24
|
+
import { registerTabletopTools } from "./tools/tabletop.js";
|
|
23
25
|
import { registerResources } from "./tools/resources.js";
|
|
24
26
|
import { applyOutputSchemas } from "./schemas.js";
|
|
25
27
|
import { taskStore, TASK_CAPABILITIES } from "./tasks.js";
|
|
@@ -43,7 +45,7 @@ async function main() {
|
|
|
43
45
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
44
46
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
45
47
|
// godot://… resources and receive notifications/resources/updated.
|
|
46
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
48
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.11.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
47
49
|
// B1: enforce frozen output schemas on every structured tool. Must run before
|
|
48
50
|
// the register*Tools calls below — it wraps server.registerTool.
|
|
49
51
|
applyOutputSchemas(server);
|
|
@@ -65,6 +67,11 @@ async function main() {
|
|
|
65
67
|
const processes = registerProcessTools(server, config);
|
|
66
68
|
// Group K: host-side knowledge & search (project grep, symbol/usage index, idiom lookup).
|
|
67
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);
|
|
68
75
|
// Group J: AI asset generation (delegated backend / connected client; degrades
|
|
69
76
|
// to a request spec when no backend is configured). Writes + imports via the bridge.
|
|
70
77
|
registerAssetGenTools(server, bridge, config);
|
|
@@ -76,6 +83,11 @@ async function main() {
|
|
|
76
83
|
// Detects the installed SDK (SilentWolf/Nakama/PlayFab/Photon) and generates gated
|
|
77
84
|
// GDScript against it; degrades cleanly when the SDK is absent or lacks the feature.
|
|
78
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);
|
|
79
91
|
// Phase 4: MCP resources (scene tree, editor state, runtime tree/log, ClassDB docs).
|
|
80
92
|
registerResources(server, bridge, runtime);
|
|
81
93
|
// D3: resource subscriptions — push notifications/resources/updated when a
|
|
@@ -118,6 +130,8 @@ function printUsage() {
|
|
|
118
130
|
" --client <id> Write the MCP config for a client: claude-code | claude-desktop | cursor | windsurf | vscode.",
|
|
119
131
|
" --force Overwrite an addon that is already installed.",
|
|
120
132
|
" --dry-run Print what would change without writing anything.",
|
|
133
|
+
" --from-github [ref] Fetch the editor addon from GitHub at [ref] (default: this package's version tag) instead of the bundled copy.",
|
|
134
|
+
" --repo <owner/repo> With --from-github, the source repo (default: jlivingston-Cipher/godot-breakpoint-mcp).",
|
|
121
135
|
"",
|
|
122
136
|
"doctor options:",
|
|
123
137
|
" --project <dir> Project to check (default: $GODOT_PROJECT or the current directory).",
|
package/dist/schemas.js
CHANGED
|
@@ -480,6 +480,95 @@ export const outputSchemas = {
|
|
|
480
480
|
})),
|
|
481
481
|
available: z.array(z.string()),
|
|
482
482
|
},
|
|
483
|
+
// Group L — version control (tools/vcs.ts). Read-only git wrappers; shapes frozen
|
|
484
|
+
// from the host reshaping in vcs.ts (git status --porcelain=v2 / log / diff / show
|
|
485
|
+
// / branch / blame --line-porcelain).
|
|
486
|
+
vcs_status: {
|
|
487
|
+
branch: z.string().nullable(),
|
|
488
|
+
oid: z.string().nullable(),
|
|
489
|
+
upstream: z.string().nullable(),
|
|
490
|
+
ahead: z.number(),
|
|
491
|
+
behind: z.number(),
|
|
492
|
+
staged: z.array(z.object({ path: z.string(), status: z.string() })),
|
|
493
|
+
unstaged: z.array(z.object({ path: z.string(), status: z.string() })),
|
|
494
|
+
untracked: z.array(z.string()),
|
|
495
|
+
unmerged: z.array(z.string()),
|
|
496
|
+
clean: z.boolean(),
|
|
497
|
+
},
|
|
498
|
+
vcs_log: {
|
|
499
|
+
commits: z.array(z.object({
|
|
500
|
+
hash: z.string(), short: z.string(), author: z.string(), date: z.string(), subject: z.string(),
|
|
501
|
+
})),
|
|
502
|
+
count: z.number(),
|
|
503
|
+
},
|
|
504
|
+
vcs_diff: {
|
|
505
|
+
staged: z.boolean(),
|
|
506
|
+
path: z.string().nullable(),
|
|
507
|
+
files: z.array(z.string()),
|
|
508
|
+
patch: z.string(),
|
|
509
|
+
truncated: z.boolean(),
|
|
510
|
+
},
|
|
511
|
+
// vcs_show has two modes (commit vs file-at-ref); only `ref` is always present, the
|
|
512
|
+
// rest are populated per mode, so all but `ref` are optional.
|
|
513
|
+
vcs_show: {
|
|
514
|
+
ref: z.string(),
|
|
515
|
+
hash: z.string().optional(),
|
|
516
|
+
short: z.string().optional(),
|
|
517
|
+
author: z.string().optional(),
|
|
518
|
+
date: z.string().optional(),
|
|
519
|
+
subject: z.string().optional(),
|
|
520
|
+
body: z.string().optional(),
|
|
521
|
+
patch: z.string().optional(),
|
|
522
|
+
path: z.string().optional(),
|
|
523
|
+
content: z.string().optional(),
|
|
524
|
+
truncated: z.boolean(),
|
|
525
|
+
},
|
|
526
|
+
vcs_branch_list: {
|
|
527
|
+
current: z.string().nullable(),
|
|
528
|
+
branches: z.array(z.object({
|
|
529
|
+
name: z.string(), short_sha: z.string(), current: z.boolean(), remote: z.boolean(),
|
|
530
|
+
})),
|
|
531
|
+
count: z.number(),
|
|
532
|
+
},
|
|
533
|
+
vcs_blame: {
|
|
534
|
+
path: z.string(),
|
|
535
|
+
lines: z.array(z.object({
|
|
536
|
+
line: z.number(), commit: z.string(), author: z.string(), date: z.string(), text: z.string(),
|
|
537
|
+
})),
|
|
538
|
+
count: z.number(),
|
|
539
|
+
truncated: z.boolean(),
|
|
540
|
+
},
|
|
541
|
+
// Group L mutating tools (Tier A). Success shapes only; gated tools that are
|
|
542
|
+
// blocked/declined return isError (exempt from output-schema validation).
|
|
543
|
+
vcs_add: {
|
|
544
|
+
staged: z.array(z.object({ path: z.string(), status: z.string() })),
|
|
545
|
+
count: z.number(),
|
|
546
|
+
},
|
|
547
|
+
vcs_commit: {
|
|
548
|
+
committed: z.boolean(),
|
|
549
|
+
hash: z.string(),
|
|
550
|
+
short: z.string(),
|
|
551
|
+
summary: z.string(),
|
|
552
|
+
},
|
|
553
|
+
vcs_restore: {
|
|
554
|
+
restored: z.array(z.string()),
|
|
555
|
+
count: z.number(),
|
|
556
|
+
},
|
|
557
|
+
vcs_stash: {
|
|
558
|
+
op: z.string(),
|
|
559
|
+
message: z.string(),
|
|
560
|
+
stashes: z.array(z.object({ ref: z.string(), description: z.string() })),
|
|
561
|
+
},
|
|
562
|
+
vcs_branch_create: {
|
|
563
|
+
created: z.boolean(),
|
|
564
|
+
name: z.string(),
|
|
565
|
+
from: z.string().nullable(),
|
|
566
|
+
switched: z.boolean(),
|
|
567
|
+
},
|
|
568
|
+
vcs_switch: {
|
|
569
|
+
switched: z.boolean(),
|
|
570
|
+
branch: z.string(),
|
|
571
|
+
},
|
|
483
572
|
// ClassDB-backed reference tools (tools/editor.ts -> operations.gd _classdb_reference / _docs_search).
|
|
484
573
|
class_reference: {
|
|
485
574
|
class: z.string(),
|
|
@@ -582,6 +671,132 @@ export const outputSchemas = {
|
|
|
582
671
|
auth_scaffold: backendScaffold,
|
|
583
672
|
};
|
|
584
673
|
})(),
|
|
674
|
+
// ---- Group N: card/board/piece authoring composites (tools/tabletop.ts) ----
|
|
675
|
+
card_template_create: {
|
|
676
|
+
scene_path: z.string(),
|
|
677
|
+
script_path: z.string(),
|
|
678
|
+
root_type: z.string(),
|
|
679
|
+
has_back: z.boolean(),
|
|
680
|
+
node_count: z.number(),
|
|
681
|
+
saved: z.boolean(),
|
|
682
|
+
slots: z.array(z.object({ name: z.string(), node_path: z.string(), kind: z.string() })),
|
|
683
|
+
},
|
|
684
|
+
card_instance: {
|
|
685
|
+
instance_path: z.string(),
|
|
686
|
+
face_up: z.boolean(),
|
|
687
|
+
bound: z.array(z.string()),
|
|
688
|
+
unbound: z.array(z.string()),
|
|
689
|
+
},
|
|
690
|
+
card_hand_layout: {
|
|
691
|
+
container_path: z.string(),
|
|
692
|
+
mode: z.string(),
|
|
693
|
+
count: z.number(),
|
|
694
|
+
instances: z.array(z.object({ index: z.number(), instance_path: z.string() })),
|
|
695
|
+
},
|
|
696
|
+
card_deck_from_table: {
|
|
697
|
+
deck_container: z.string(),
|
|
698
|
+
count: z.number(),
|
|
699
|
+
rows_read: z.number(),
|
|
700
|
+
rows_skipped: z.number(),
|
|
701
|
+
unmapped_columns: z.array(z.string()),
|
|
702
|
+
instances: z.array(z.object({ row_index: z.number(), instance_path: z.string() })),
|
|
703
|
+
},
|
|
704
|
+
card_set_face: {
|
|
705
|
+
node_path: z.string(),
|
|
706
|
+
face_up: z.boolean(),
|
|
707
|
+
method: z.string(),
|
|
708
|
+
animated: z.boolean(),
|
|
709
|
+
player_path: z.string().nullable(),
|
|
710
|
+
anim: z.string().nullable(),
|
|
711
|
+
},
|
|
712
|
+
board_create: {
|
|
713
|
+
scene_path: z.string(),
|
|
714
|
+
root_type: z.string(),
|
|
715
|
+
cell_kind: z.string(),
|
|
716
|
+
layout_mode: z.string(),
|
|
717
|
+
cell_count: z.number(),
|
|
718
|
+
node_count: z.number(),
|
|
719
|
+
saved: z.boolean(),
|
|
720
|
+
cells: z.array(z.object({ id: z.string(), node_path: z.string(), x: z.number(), y: z.number() })),
|
|
721
|
+
},
|
|
722
|
+
board_place: {
|
|
723
|
+
placed: z.boolean(),
|
|
724
|
+
cell: z.string(),
|
|
725
|
+
cell_path: z.string(),
|
|
726
|
+
node_path: z.string(),
|
|
727
|
+
align: z.object({ x: z.number(), y: z.number() }),
|
|
728
|
+
},
|
|
729
|
+
board_tile_create: {
|
|
730
|
+
scene_path: z.string(),
|
|
731
|
+
layer_path: z.string(),
|
|
732
|
+
layer_name: z.string(),
|
|
733
|
+
rows: z.number(),
|
|
734
|
+
cols: z.number(),
|
|
735
|
+
tile_size: z.array(z.number()),
|
|
736
|
+
tileset_path: z.string(),
|
|
737
|
+
tileset_created: z.boolean(),
|
|
738
|
+
cell_count: z.number(),
|
|
739
|
+
painted: z.boolean(),
|
|
740
|
+
node_count: z.number(),
|
|
741
|
+
saved: z.boolean(),
|
|
742
|
+
},
|
|
743
|
+
board_tile_place: {
|
|
744
|
+
placed: z.boolean(),
|
|
745
|
+
coord: z.array(z.number()),
|
|
746
|
+
layer_path: z.string(),
|
|
747
|
+
node_path: z.string(),
|
|
748
|
+
local_pos: z.object({ x: z.number(), y: z.number() }),
|
|
749
|
+
tile_size: z.array(z.number()),
|
|
750
|
+
anchor: z.string(),
|
|
751
|
+
align: z.object({ x: z.number(), y: z.number() }),
|
|
752
|
+
reparented: z.boolean(),
|
|
753
|
+
},
|
|
754
|
+
piece_template_create: {
|
|
755
|
+
scene_path: z.string(),
|
|
756
|
+
script_path: z.string(),
|
|
757
|
+
root_type: z.string(),
|
|
758
|
+
has_label: z.boolean(),
|
|
759
|
+
has_hit_area: z.boolean(),
|
|
760
|
+
has_back: z.boolean(),
|
|
761
|
+
node_count: z.number(),
|
|
762
|
+
saved: z.boolean(),
|
|
763
|
+
nodes: z.array(z.object({ name: z.string(), node_path: z.string(), type: z.string() })),
|
|
764
|
+
},
|
|
765
|
+
piece_instance: {
|
|
766
|
+
instance_path: z.string(),
|
|
767
|
+
face_up: z.boolean(),
|
|
768
|
+
bound: z.array(z.string()),
|
|
769
|
+
unbound: z.array(z.string()),
|
|
770
|
+
placed: z.boolean(),
|
|
771
|
+
cell: z.string().nullable(),
|
|
772
|
+
},
|
|
773
|
+
piece_move: {
|
|
774
|
+
moved: z.boolean(),
|
|
775
|
+
from: z.string().nullable(),
|
|
776
|
+
to: z.string(),
|
|
777
|
+
node_path: z.string(),
|
|
778
|
+
animated: z.boolean(),
|
|
779
|
+
},
|
|
780
|
+
interact_make_draggable: {
|
|
781
|
+
node_path: z.string(),
|
|
782
|
+
mode: z.string(),
|
|
783
|
+
script_path: z.string(),
|
|
784
|
+
payload_keys: z.array(z.string()),
|
|
785
|
+
action: z.string().nullable(),
|
|
786
|
+
connected: z.boolean(),
|
|
787
|
+
composed: z.boolean(),
|
|
788
|
+
base_script: z.string().nullable(),
|
|
789
|
+
},
|
|
790
|
+
interact_add_drop_zone: {
|
|
791
|
+
node_path: z.string(),
|
|
792
|
+
mode: z.string(),
|
|
793
|
+
script_path: z.string(),
|
|
794
|
+
on_drop: z.string(),
|
|
795
|
+
accepts_key: z.string(),
|
|
796
|
+
accepts_values: z.array(z.string()),
|
|
797
|
+
notified: z.boolean(),
|
|
798
|
+
area_path: z.string().nullable(),
|
|
799
|
+
},
|
|
585
800
|
};
|
|
586
801
|
/**
|
|
587
802
|
* Inject the frozen output schemas into every matching tool at registration
|