breakpoint-mcp 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/tools.js +1 -20
- package/dist/cslsp.js +2 -1
- package/dist/index.js +1 -1
- package/dist/lsp.js +2 -1
- package/dist/ports.js +45 -11
- package/dist/tools/csdap.js +37 -3
- package/dist/tools/dap.js +30 -2
- package/dist/version.js +32 -0
- package/package.json +1 -1
package/dist/cli/tools.js
CHANGED
|
@@ -28,26 +28,7 @@ import { applyOutputSchemas } from "../schemas.js";
|
|
|
28
28
|
import { applyAnnotations, annotationsFor } from "../annotations.js";
|
|
29
29
|
import { TOOL_CAPABILITIES, CAPABILITY_GROUPS, GROUP_DESCRIBE } from "../capabilities.js";
|
|
30
30
|
import { loadConfig } from "../config.js";
|
|
31
|
-
import {
|
|
32
|
-
import { fileURLToPath } from "node:url";
|
|
33
|
-
import { dirname, join } from "node:path";
|
|
34
|
-
/**
|
|
35
|
-
* Read the version from package.json at runtime rather than hardcoding it.
|
|
36
|
-
* Deliberate: the version already has to be bumped in `package.json`, the
|
|
37
|
-
* `serverInfo` literal, both `plugin.cfg`s, and the doctor/init surface strings
|
|
38
|
-
* — this command must not become a sixth place to forget. package.json ships in
|
|
39
|
-
* the npm tarball, so the read is safe in an installed package; if it ever
|
|
40
|
-
* isn't, an unknown version is better than a stale one.
|
|
41
|
-
*/
|
|
42
|
-
function packageVersion() {
|
|
43
|
-
try {
|
|
44
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
45
|
-
return JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf8")).version ?? "unknown";
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return "unknown";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
31
|
+
import { packageVersion } from "../version.js";
|
|
51
32
|
/**
|
|
52
33
|
* Register the whole surface against a recorder, exactly as `index.ts` does
|
|
53
34
|
* (`applyOutputSchemas` → `applyAnnotations` → every `register*Tools`), and
|
package/dist/cslsp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { LspError } from "./lsp.js";
|
|
3
|
+
import { packageVersion } from "./version.js";
|
|
3
4
|
/**
|
|
4
5
|
* Minimal LSP client for the C#/.NET semantic plane (D4 C2). Transport-agnostic:
|
|
5
6
|
* it drives any `JsonRpcChannel`, so production spawns OmniSharp over stdio
|
|
@@ -164,7 +165,7 @@ export class CsLspClient {
|
|
|
164
165
|
workspace: { symbol: {}, workspaceFolders: true },
|
|
165
166
|
},
|
|
166
167
|
workspaceFolders: [{ uri: this.rootUri, name: "csharp-project" }],
|
|
167
|
-
clientInfo: { name: "breakpoint-mcp", version:
|
|
168
|
+
clientInfo: { name: "breakpoint-mcp", version: packageVersion() },
|
|
168
169
|
}));
|
|
169
170
|
this.serverCapabilities = result?.capabilities ?? {};
|
|
170
171
|
await this.notify("initialized", {});
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ async function main() {
|
|
|
36
36
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
37
37
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
38
38
|
// godot://… resources and receive notifications/resources/updated.
|
|
39
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
39
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.26.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
40
40
|
// B1: enforce frozen output schemas on every structured tool. Must run before
|
|
41
41
|
// any register*Tools call — it wraps server.registerTool.
|
|
42
42
|
applyOutputSchemas(server);
|
package/dist/lsp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { FramedConnection } from "./framing.js";
|
|
3
|
+
import { packageVersion } from "./version.js";
|
|
3
4
|
export class LspError extends Error {
|
|
4
5
|
code;
|
|
5
6
|
constructor(code, message) {
|
|
@@ -165,7 +166,7 @@ export class LspClient {
|
|
|
165
166
|
workspace: { symbol: {}, workspaceFolders: true },
|
|
166
167
|
},
|
|
167
168
|
workspaceFolders: [{ uri: this.rootUri, name: "godot-project" }],
|
|
168
|
-
clientInfo: { name: "breakpoint-mcp", version:
|
|
169
|
+
clientInfo: { name: "breakpoint-mcp", version: packageVersion() },
|
|
169
170
|
}));
|
|
170
171
|
this.serverCapabilities = result?.capabilities ?? {};
|
|
171
172
|
await this.notify("initialized", {});
|
package/dist/ports.js
CHANGED
|
@@ -31,15 +31,16 @@ export function portFree(host, port) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* The refusal text shared by
|
|
34
|
+
* The refusal text shared by every tool that starts a game.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
37
|
-
* (`runtime_bridge.gd:74`
|
|
38
|
-
* `listen()` returns non-OK, it
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `runtime_*` call then answers
|
|
42
|
-
* carries no pid or boot nonce
|
|
36
|
+
* All of them launch a project whose runtime autoload binds
|
|
37
|
+
* `BREAKPOINT_RUNTIME_PORT` (`runtime_bridge.gd:74` reads it, `:79` binds it).
|
|
38
|
+
* When that port is already held, the autoload's `listen()` returns non-OK, it
|
|
39
|
+
* `push_error`s, and **the game keeps running without a bridge** — while the
|
|
40
|
+
* host's runtime client, which dials the same fixed port, connects to whichever
|
|
41
|
+
* process got there first. Every subsequent `runtime_*` call then answers
|
|
42
|
+
* confidently about the WRONG process, and `ping` carries no pid or boot nonce
|
|
43
|
+
* that could tell them apart.
|
|
43
44
|
*
|
|
44
45
|
* That is why this refuses rather than warns: a determinism feature returning a
|
|
45
46
|
* correct-looking answer from the wrong game is worse than one that will not
|
|
@@ -52,11 +53,44 @@ export function portFree(host, port) {
|
|
|
52
53
|
* several games at once, which is true; `runtime_peer_stop` as a way to free
|
|
53
54
|
* THIS port would not be, and a remedy that cannot work is worse than one fewer
|
|
54
55
|
* suggestion.
|
|
56
|
+
*
|
|
57
|
+
* The `debugger` variant differs in one way that matters. A DAP session is
|
|
58
|
+
* addressed by SESSION, not by port, so `dbg_*` keeps working perfectly against
|
|
59
|
+
* a second game even with the port held — only `runtime_*` is corrupted. The
|
|
60
|
+
* override is therefore a legitimate everyday choice on that plane rather than a
|
|
61
|
+
* last resort, and the text says so instead of over-warning. Over-warning is how
|
|
62
|
+
* a check earns the reputation that gets it disabled.
|
|
63
|
+
*
|
|
64
|
+
* What NEITHER variant may do is claim to know WHAT is holding the port. All the
|
|
65
|
+
* probe learns is that something is. The holder may be a `godot_run_managed`
|
|
66
|
+
* child (`godot_stop` clears it), a game already under the debugger (`dbg_attach`
|
|
67
|
+
* reaches it), or a window the developer opened themselves (only they can close
|
|
68
|
+
* it) — and the two planes contend for the SAME port, so a debugger refusal is
|
|
69
|
+
* often about a run-plane holder and vice versa. An earlier draft of the
|
|
70
|
+
* debugger text asserted "a debugger-launched game is owned by the editor, so no
|
|
71
|
+
* tool here can stop it" and withheld `godot_stop` on that basis; it was wrong
|
|
72
|
+
* whenever the holder was a managed child, which is the commonest case of all.
|
|
73
|
+
* Both variants now list every remedy with the condition under which it applies,
|
|
74
|
+
* and assert nothing about which one is live.
|
|
55
75
|
*/
|
|
56
|
-
export function portConflictMessage(host, port) {
|
|
57
|
-
|
|
76
|
+
export function portConflictMessage(host, port, launcher = "run") {
|
|
77
|
+
const why = `${host}:${port} is already bound, so a game started now could not host the runtime bridge. ` +
|
|
58
78
|
`Its autoload would fail to listen and keep running anyway, and every runtime_* call would ` +
|
|
59
|
-
`silently address the process that already holds the port instead of the one you just started.
|
|
79
|
+
`silently address the process that already holds the port instead of the one you just started. `;
|
|
80
|
+
if (launcher === "debugger") {
|
|
81
|
+
return (why +
|
|
82
|
+
`Deal with whatever is already holding it — which remedy applies depends on what it is: ` +
|
|
83
|
+
`godot_stop if godot_run_managed started it, dbg_attach / cs_dbg_attach to debug the running ` +
|
|
84
|
+
`game instead of launching a second one (that works only if it is already under the ` +
|
|
85
|
+
`debugger — a plain godot_run_project or godot_run_managed game is not), or quit it in the ` +
|
|
86
|
+
`game window or the editor. You can also point this server at a free port with ` +
|
|
87
|
+
`BREAKPOINT_RUNTIME_PORT. Pass allow_port_conflict:true to launch anyway: breakpoints, ` +
|
|
88
|
+
`stepping and variable inspection will all work normally, because a DAP session is addressed ` +
|
|
89
|
+
`by session rather than by port — but every runtime_* call would go to the process holding ` +
|
|
90
|
+
`the port, not to the game you just launched. To drive more than one game at once, use ` +
|
|
91
|
+
`runtime_spawn_peers, which allocates a distinct port per peer.`);
|
|
92
|
+
}
|
|
93
|
+
return (why +
|
|
60
94
|
`Stop the running game first — godot_stop if godot_run_managed started it, otherwise quit it in ` +
|
|
61
95
|
`the game window or end the debug session that launched it (a detached godot_run_project game is ` +
|
|
62
96
|
`not stoppable by any tool) — or point this server at a free port with ` +
|
package/dist/tools/csdap.js
CHANGED
|
@@ -3,6 +3,7 @@ import { DapError } from "../dap.js";
|
|
|
3
3
|
import { toFsPath } from "../paths.js";
|
|
4
4
|
import { gate } from "../confirm.js";
|
|
5
5
|
import { ok } from "./lsp-common.js";
|
|
6
|
+
import { portFree, portConflictMessage } from "../ports.js";
|
|
6
7
|
// How long step/continue wait for the program to settle (hit a breakpoint,
|
|
7
8
|
// finish a step, or terminate) before returning. On timeout the tool reports
|
|
8
9
|
// the current state — e.g. `continue` with no further breakpoint stays running.
|
|
@@ -54,12 +55,41 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
54
55
|
args: z.array(z.string()).optional().describe("Program arguments (default: ['--path', <C# project>])"),
|
|
55
56
|
stop_on_entry: z.boolean().optional().describe("Break at entry (default false)"),
|
|
56
57
|
just_my_code: z.boolean().optional().describe("Restrict stepping/breakpoints to user code (netcoredbg justMyCode; default true)"),
|
|
58
|
+
allow_port_conflict: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("Launch even though the runtime bridge port is already bound (default false). Only consulted when " +
|
|
62
|
+
"this looks like a Godot launch — the program is named godot, or the args carry Godot's --path " +
|
|
63
|
+
"project flag. Debugging some other .NET program is never gated. Breakpoints and stepping still " +
|
|
64
|
+
"work; runtime_* tools would talk to the process holding the port."),
|
|
57
65
|
},
|
|
58
|
-
}, async ({ program, args, stop_on_entry, just_my_code }) => {
|
|
66
|
+
}, async ({ program, args, stop_on_entry, just_my_code, allow_port_conflict }) => {
|
|
67
|
+
const resolvedProgram = program ?? cfg.csDapProgram;
|
|
68
|
+
const resolvedArgs = args ?? ["--path", cfg.csDapProjectPath];
|
|
69
|
+
// Does this launch a Godot game — i.e. one whose autoload wants the runtime
|
|
70
|
+
// port — or some other .NET program netcoredbg is being pointed at?
|
|
71
|
+
//
|
|
72
|
+
// DEFAULT TO YES, and skip the gate only when the caller has clearly aimed
|
|
73
|
+
// elsewhere. Comparing the resolved program against `cfg.csDapProgram` was
|
|
74
|
+
// the obvious-looking test and is wrong: that is equality against the
|
|
75
|
+
// DEFAULT, so passing the real Mono binary explicitly — the way config.ts
|
|
76
|
+
// documents pointing at it — skipped the gate on the mainline path this
|
|
77
|
+
// whole change exists to cover. `--path <project>` is Godot's own project
|
|
78
|
+
// flag and appears in the default args; a program not called godot and not
|
|
79
|
+
// given --path is the one case we are confident is not a game.
|
|
80
|
+
const looksLikeGodot = /godot/i.test(resolvedProgram.split(/[\\/]/).pop() ?? "") || resolvedArgs.includes("--path");
|
|
81
|
+
if (looksLikeGodot &&
|
|
82
|
+
!allow_port_conflict &&
|
|
83
|
+
!(await portFree(cfg.runtimeHost, cfg.runtimePort))) {
|
|
84
|
+
return {
|
|
85
|
+
isError: true,
|
|
86
|
+
content: [{ type: "text", text: portConflictMessage(cfg.runtimeHost, cfg.runtimePort, "debugger") }],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
59
89
|
try {
|
|
60
90
|
await dap.start("launch", {
|
|
61
|
-
program:
|
|
62
|
-
args:
|
|
91
|
+
program: resolvedProgram,
|
|
92
|
+
args: resolvedArgs,
|
|
63
93
|
cwd: cfg.csDapProjectPath,
|
|
64
94
|
stopAtEntry: stop_on_entry ?? false,
|
|
65
95
|
justMyCode: just_my_code ?? true,
|
|
@@ -70,6 +100,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
70
100
|
return fail(err);
|
|
71
101
|
}
|
|
72
102
|
});
|
|
103
|
+
// NOT port-gated, deliberately — see dbg_attach: attaching to the process that
|
|
104
|
+
// already holds the port is the remedy, not the problem.
|
|
73
105
|
server.registerTool("cs_dbg_attach", {
|
|
74
106
|
title: "Attach C# debug session",
|
|
75
107
|
description: "Attach netcoredbg to an already-running .NET process (e.g. a C# Godot game launched separately) by its OS process id. " +
|
|
@@ -345,6 +377,8 @@ export function registerCsDapTools(server, dap, cfg) {
|
|
|
345
377
|
return fail(err);
|
|
346
378
|
}
|
|
347
379
|
});
|
|
380
|
+
// NOT port-gated, deliberately — see dbg_restart: the session's own game still
|
|
381
|
+
// holds the port at check time, so a probe here false-positives every restart.
|
|
348
382
|
server.registerTool("cs_dbg_restart", {
|
|
349
383
|
title: "Restart C# debug session",
|
|
350
384
|
description: "Restart the current C# debug session. Uses the DAP `restart` request when the adapter advertises `supportsRestartRequest`, " +
|
package/dist/tools/dap.js
CHANGED
|
@@ -3,6 +3,7 @@ import { DapError } from "../dap.js";
|
|
|
3
3
|
import { toFsPath } from "../paths.js";
|
|
4
4
|
import { gate } from "../confirm.js";
|
|
5
5
|
import { ok } from "./lsp-common.js";
|
|
6
|
+
import { portFree, portConflictMessage } from "../ports.js";
|
|
6
7
|
// How long step/continue wait for the program to settle (hit a breakpoint,
|
|
7
8
|
// finish a step, or terminate) before returning. On timeout the tool reports
|
|
8
9
|
// the current state — e.g. `continue` with no further breakpoint stays running.
|
|
@@ -55,12 +56,28 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
55
56
|
server.registerTool("dbg_launch", {
|
|
56
57
|
title: "Launch debug session",
|
|
57
58
|
description: "Start the game under the debugger. scene may be 'main', 'current', or a res:// scene path. " +
|
|
58
|
-
"Any breakpoints set beforehand are applied during the handshake."
|
|
59
|
+
"Any breakpoints set beforehand are applied during the handshake. " +
|
|
60
|
+
"Refuses if the runtime bridge port is already bound — the new game could not host the bridge, so " +
|
|
61
|
+
"runtime_* would address the process already holding the port. Clear the holder (godot_stop, or quit " +
|
|
62
|
+
"it), or dbg_attach onto it if it is already under the debugger, or pass allow_port_conflict " +
|
|
63
|
+
"(dbg_* is unaffected either way; only runtime_* is).",
|
|
59
64
|
inputSchema: {
|
|
60
65
|
scene: z.string().optional().describe("'main' (default), 'current', or res://scene.tscn"),
|
|
61
66
|
stop_on_entry: z.boolean().optional().describe("Break at entry (default false)"),
|
|
67
|
+
allow_port_conflict: z
|
|
68
|
+
.boolean()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Launch even though the runtime bridge port is already bound (default false). Breakpoints and " +
|
|
71
|
+
"stepping still work — a DAP session is addressed by session, not by port — but runtime_* " +
|
|
72
|
+
"tools would talk to the process holding the port, not to this game."),
|
|
62
73
|
},
|
|
63
|
-
}, async ({ scene, stop_on_entry }) => {
|
|
74
|
+
}, async ({ scene, stop_on_entry, allow_port_conflict }) => {
|
|
75
|
+
if (!allow_port_conflict && !(await portFree(cfg.runtimeHost, cfg.runtimePort))) {
|
|
76
|
+
return {
|
|
77
|
+
isError: true,
|
|
78
|
+
content: [{ type: "text", text: portConflictMessage(cfg.runtimeHost, cfg.runtimePort, "debugger") }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
64
81
|
try {
|
|
65
82
|
await dap.start("launch", {
|
|
66
83
|
project: cfg.projectPath,
|
|
@@ -73,6 +90,9 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
73
90
|
return fail(err);
|
|
74
91
|
}
|
|
75
92
|
});
|
|
93
|
+
// NOT port-gated, deliberately: attaching to the game that already holds the
|
|
94
|
+
// port is the REMEDY dbg_launch's refusal points at. Gating it would close the
|
|
95
|
+
// exit and leave the caller with nowhere to go.
|
|
76
96
|
server.registerTool("dbg_attach", {
|
|
77
97
|
title: "Attach debug session",
|
|
78
98
|
description: "Attach to an already-running Godot debug session.",
|
|
@@ -367,6 +387,14 @@ export function registerDapTools(server, dap, cfg) {
|
|
|
367
387
|
return fail(err);
|
|
368
388
|
}
|
|
369
389
|
});
|
|
390
|
+
// NOT port-gated, deliberately, and this one is a trap worth naming: at the
|
|
391
|
+
// moment of the check the session's OWN game still holds the runtime port and
|
|
392
|
+
// is about to be terminated. A probe here would fire on the very process it is
|
|
393
|
+
// about to replace — a guaranteed false positive on the happy path, every
|
|
394
|
+
// single time. The relaunch reuses the launch parameters, so if some THIRD
|
|
395
|
+
// process holds the port the fresh game lands bridgeless exactly as before;
|
|
396
|
+
// that residue is accepted knowingly rather than traded for a check that cries
|
|
397
|
+
// wolf on every restart.
|
|
370
398
|
server.registerTool("dbg_restart", {
|
|
371
399
|
title: "Restart debug session",
|
|
372
400
|
description: "Restart the current debug session. Uses the DAP `restart` request when the adapter advertises `supportsRestartRequest`, " +
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* The package version, read at runtime rather than hardcoded.
|
|
6
|
+
*
|
|
7
|
+
* Every literal version string in the source is a place a release can forget,
|
|
8
|
+
* and `contract_check.py` check 14 exists because two consecutive releases did.
|
|
9
|
+
* The best outcome is not a gated literal but no literal at all — so anything
|
|
10
|
+
* that merely needs to *report* the version should call this instead of being
|
|
11
|
+
* added to the roster.
|
|
12
|
+
*
|
|
13
|
+
* This was not academic. `lsp.ts` and `cslsp.ts` announced
|
|
14
|
+
* `clientInfo: { version: "0.2.0" }` to Godot's language server and to
|
|
15
|
+
* OmniSharp from the initial commit until 1.26.0 — the project shipped
|
|
16
|
+
* twenty-odd releases telling every LSP server it was 0.2.0, and nothing
|
|
17
|
+
* noticed, because a literal nobody compares to anything cannot go stale
|
|
18
|
+
* loudly.
|
|
19
|
+
*
|
|
20
|
+
* `package.json` ships in the npm tarball, so the read is safe in an installed
|
|
21
|
+
* package; if it ever isn't, an unknown version is better than a stale one.
|
|
22
|
+
*/
|
|
23
|
+
export function packageVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "unknown";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "MCP server exposing Godot to AI coding assistants over the Model Context Protocol — developed and tested with Claude — across four planes (CLI, live editor, LSP+DAP, runtime) with elicitation-gated destructive tools, long jobs on the formal MCP task model, captured console output, and MCP resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|