breakpoint-mcp 1.0.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/bridge.js +189 -0
- package/dist/config.js +71 -0
- package/dist/confirm.js +45 -0
- package/dist/csdap.js +331 -0
- package/dist/cslsp.js +217 -0
- package/dist/dap.js +341 -0
- package/dist/framing.js +125 -0
- package/dist/index.js +110 -0
- package/dist/logger.js +11 -0
- package/dist/lsp.js +219 -0
- package/dist/paths.js +28 -0
- package/dist/schemas.js +587 -0
- package/dist/stdio.js +101 -0
- package/dist/subscriptions.js +141 -0
- package/dist/tasks.js +146 -0
- package/dist/tools/assetgen.js +360 -0
- package/dist/tools/backend.js +532 -0
- package/dist/tools/cli.js +130 -0
- package/dist/tools/csdap.js +377 -0
- package/dist/tools/cslsp.js +285 -0
- package/dist/tools/dap.js +504 -0
- package/dist/tools/editor.js +1919 -0
- package/dist/tools/knowledge.js +517 -0
- package/dist/tools/lsp-common.js +121 -0
- package/dist/tools/lsp.js +576 -0
- package/dist/tools/netcode.js +411 -0
- package/dist/tools/processes.js +103 -0
- package/dist/tools/resources.js +27 -0
- package/dist/tools/runtime.js +125 -0
- package/package.json +57 -0
package/dist/lsp.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { FramedConnection } from "./framing.js";
|
|
3
|
+
export class LspError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "LspError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Minimal LSP client for Godot's GDScript language server (raw TCP + JSON-RPC
|
|
13
|
+
* 2.0). Handles the initialize handshake, textDocument/didOpen, request/response
|
|
14
|
+
* correlation, and caches published diagnostics per document URI.
|
|
15
|
+
*/
|
|
16
|
+
export class LspClient {
|
|
17
|
+
rootUri;
|
|
18
|
+
timeoutMs;
|
|
19
|
+
conn;
|
|
20
|
+
nextId = 1;
|
|
21
|
+
pending = new Map();
|
|
22
|
+
initialized = null;
|
|
23
|
+
serverCapabilities = null;
|
|
24
|
+
opened = new Set();
|
|
25
|
+
diagnostics = new Map();
|
|
26
|
+
diagWaiters = new Map();
|
|
27
|
+
/** Absolute project root path (no trailing slash), used to canonicalize URIs. */
|
|
28
|
+
rootFsPath;
|
|
29
|
+
constructor(host, port, rootUri, timeoutMs) {
|
|
30
|
+
this.rootUri = rootUri;
|
|
31
|
+
this.timeoutMs = timeoutMs;
|
|
32
|
+
let root = "";
|
|
33
|
+
try {
|
|
34
|
+
root = fileURLToPath(rootUri);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
root = rootUri.replace(/^file:\/\//, "");
|
|
38
|
+
}
|
|
39
|
+
this.rootFsPath = root.replace(/[\\/]+$/, "");
|
|
40
|
+
this.conn = new FramedConnection(host, port, "LSP", "Is the editor running with the GDScript language server enabled (Editor Settings → Network → Language Server, port 6005)?");
|
|
41
|
+
this.conn.onMessage((m) => this.onMessage(m));
|
|
42
|
+
this.conn.onClose(() => this.onClose());
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reduce any document URI to a stable, project-relative key (e.g. "player.gd")
|
|
46
|
+
* so a published-diagnostics URI matches the one we opened the file with —
|
|
47
|
+
* regardless of how the server spells it. Godot's language server can echo a
|
|
48
|
+
* `file://` URI with the path un-encoded (spaces literal) or, on some builds,
|
|
49
|
+
* a bare `res://` URI; neither string-equals the percent-encoded `file://`
|
|
50
|
+
* URI that Node's pathToFileURL produced. Without this, gd_diagnostics would
|
|
51
|
+
* silently time out and return empty on any project whose path needs encoding.
|
|
52
|
+
*/
|
|
53
|
+
diagKey(uri) {
|
|
54
|
+
let s = uri;
|
|
55
|
+
try {
|
|
56
|
+
s = decodeURIComponent(uri);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* keep raw on malformed encoding */
|
|
60
|
+
}
|
|
61
|
+
if (s.startsWith("res://"))
|
|
62
|
+
return s.slice("res://".length).replace(/^[\\/]+/, "");
|
|
63
|
+
if (s.startsWith("file://"))
|
|
64
|
+
s = s.slice("file://".length);
|
|
65
|
+
if (this.rootFsPath && s.startsWith(this.rootFsPath))
|
|
66
|
+
s = s.slice(this.rootFsPath.length);
|
|
67
|
+
return s.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
68
|
+
}
|
|
69
|
+
onMessage(msg) {
|
|
70
|
+
const id = msg["id"];
|
|
71
|
+
const method = msg["method"];
|
|
72
|
+
// Response to one of our requests.
|
|
73
|
+
if (typeof id === "number" && method === undefined) {
|
|
74
|
+
const p = this.pending.get(id);
|
|
75
|
+
if (!p)
|
|
76
|
+
return;
|
|
77
|
+
this.pending.delete(id);
|
|
78
|
+
clearTimeout(p.timer);
|
|
79
|
+
if (msg["error"]) {
|
|
80
|
+
const e = msg["error"];
|
|
81
|
+
p.reject(new LspError(e.code ?? -1, e.message ?? "LSP error"));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
p.resolve(msg["result"] ?? null);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Server -> client notification.
|
|
89
|
+
if (typeof method === "string" && id === undefined) {
|
|
90
|
+
if (method === "textDocument/publishDiagnostics") {
|
|
91
|
+
const params = (msg["params"] ?? {});
|
|
92
|
+
const uri = params.uri ?? "";
|
|
93
|
+
const diags = (params.diagnostics ?? []).map((d) => {
|
|
94
|
+
const dd = d;
|
|
95
|
+
return {
|
|
96
|
+
severity: dd.severity ?? 1,
|
|
97
|
+
message: dd.message ?? "",
|
|
98
|
+
line: dd.range?.start?.line ?? 0,
|
|
99
|
+
character: dd.range?.start?.character ?? 0,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const key = this.diagKey(uri);
|
|
103
|
+
this.diagnostics.set(key, diags);
|
|
104
|
+
const waiters = this.diagWaiters.get(key);
|
|
105
|
+
if (waiters) {
|
|
106
|
+
this.diagWaiters.delete(key);
|
|
107
|
+
for (const w of waiters)
|
|
108
|
+
w();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Server -> client request (e.g. client/registerCapability): ack with null
|
|
114
|
+
// so the server does not block waiting for us.
|
|
115
|
+
if (typeof method === "string" && typeof id === "number") {
|
|
116
|
+
void this.conn.send({ jsonrpc: "2.0", id, result: null });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
onClose() {
|
|
120
|
+
for (const [, p] of this.pending) {
|
|
121
|
+
clearTimeout(p.timer);
|
|
122
|
+
p.reject(new LspError("closed", "LSP connection closed"));
|
|
123
|
+
}
|
|
124
|
+
this.pending.clear();
|
|
125
|
+
this.initialized = null;
|
|
126
|
+
this.serverCapabilities = null;
|
|
127
|
+
this.opened.clear();
|
|
128
|
+
}
|
|
129
|
+
rawRequest(method, params, timeoutMs = this.timeoutMs) {
|
|
130
|
+
const id = this.nextId++;
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
this.pending.delete(id);
|
|
134
|
+
reject(new LspError("timeout", `LSP '${method}' timed out after ${timeoutMs}ms`));
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
137
|
+
this.conn.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
this.pending.delete(id);
|
|
140
|
+
reject(err);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
notify(method, params) {
|
|
145
|
+
return this.conn.send({ jsonrpc: "2.0", method, params });
|
|
146
|
+
}
|
|
147
|
+
ensureInitialized() {
|
|
148
|
+
if (!this.initialized) {
|
|
149
|
+
this.initialized = (async () => {
|
|
150
|
+
const result = (await this.rawRequest("initialize", {
|
|
151
|
+
processId: process.pid,
|
|
152
|
+
rootUri: this.rootUri,
|
|
153
|
+
rootPath: decodeURIComponent(this.rootUri.replace(/^file:\/\//, "")),
|
|
154
|
+
capabilities: {
|
|
155
|
+
textDocument: {
|
|
156
|
+
synchronization: { didSave: true, dynamicRegistration: false },
|
|
157
|
+
completion: { completionItem: { snippetSupport: false } },
|
|
158
|
+
hover: { contentFormat: ["plaintext", "markdown"] },
|
|
159
|
+
definition: {},
|
|
160
|
+
references: {},
|
|
161
|
+
rename: {},
|
|
162
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
163
|
+
publishDiagnostics: {},
|
|
164
|
+
},
|
|
165
|
+
workspace: { symbol: {}, workspaceFolders: true },
|
|
166
|
+
},
|
|
167
|
+
workspaceFolders: [{ uri: this.rootUri, name: "godot-project" }],
|
|
168
|
+
clientInfo: { name: "breakpoint-mcp", version: "0.2.0" },
|
|
169
|
+
}));
|
|
170
|
+
this.serverCapabilities = result?.capabilities ?? {};
|
|
171
|
+
await this.notify("initialized", {});
|
|
172
|
+
return result;
|
|
173
|
+
})();
|
|
174
|
+
}
|
|
175
|
+
return this.initialized;
|
|
176
|
+
}
|
|
177
|
+
async request(method, params, timeoutMs) {
|
|
178
|
+
await this.ensureInitialized();
|
|
179
|
+
return this.rawRequest(method, params, timeoutMs);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The server's advertised capabilities from the `initialize` handshake (an
|
|
183
|
+
* empty object if the server advertised none). Lets a tool feature-detect an
|
|
184
|
+
* optional LSP method — e.g. `workspaceSymbolProvider` — before calling it,
|
|
185
|
+
* instead of surfacing a raw `-32601 Method not found` to the caller.
|
|
186
|
+
*/
|
|
187
|
+
async getServerCapabilities() {
|
|
188
|
+
await this.ensureInitialized();
|
|
189
|
+
return this.serverCapabilities ?? {};
|
|
190
|
+
}
|
|
191
|
+
async ensureOpen(uri, text) {
|
|
192
|
+
if (this.opened.has(uri))
|
|
193
|
+
return;
|
|
194
|
+
await this.ensureInitialized();
|
|
195
|
+
this.opened.add(uri);
|
|
196
|
+
await this.notify("textDocument/didOpen", {
|
|
197
|
+
textDocument: { uri, languageId: "gdscript", version: 1, text },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** Return cached diagnostics for a URI, waiting up to timeoutMs for the first publish. */
|
|
201
|
+
waitForDiagnostics(uri, timeoutMs) {
|
|
202
|
+
const key = this.diagKey(uri);
|
|
203
|
+
if (this.diagnostics.has(key))
|
|
204
|
+
return Promise.resolve(this.diagnostics.get(key));
|
|
205
|
+
return new Promise((resolve) => {
|
|
206
|
+
const timer = setTimeout(() => resolve(this.diagnostics.get(key) ?? []), timeoutMs);
|
|
207
|
+
const arr = this.diagWaiters.get(key) ?? [];
|
|
208
|
+
arr.push(() => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
resolve(this.diagnostics.get(key) ?? []);
|
|
211
|
+
});
|
|
212
|
+
this.diagWaiters.set(key, arr);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
close() {
|
|
216
|
+
this.conn.close();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=lsp.js.map
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a project path that may be given as `res://...`, an absolute path, or
|
|
6
|
+
* a path relative to the project root, into an absolute filesystem path.
|
|
7
|
+
*/
|
|
8
|
+
export function toFsPath(p, projectPath) {
|
|
9
|
+
if (p.startsWith("res://"))
|
|
10
|
+
return path.join(projectPath, p.slice("res://".length));
|
|
11
|
+
if (path.isAbsolute(p))
|
|
12
|
+
return p;
|
|
13
|
+
return path.join(projectPath, p);
|
|
14
|
+
}
|
|
15
|
+
/** Same resolution as toFsPath, returned as a `file://` URI (for LSP). */
|
|
16
|
+
export function toFileUri(p, projectPath) {
|
|
17
|
+
return pathToFileURL(toFsPath(p, projectPath)).href;
|
|
18
|
+
}
|
|
19
|
+
/** Read a project file's text, or return "" if it cannot be read. */
|
|
20
|
+
export function readFileText(absPath) {
|
|
21
|
+
try {
|
|
22
|
+
return fs.readFileSync(absPath, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=paths.js.map
|