@npgamedev/godot-mcp-server 0.0.1 → 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.
Files changed (129) hide show
  1. package/ATTRIBUTIONS.md +141 -0
  2. package/LICENSE +28 -0
  3. package/README.md +386 -4
  4. package/dist/extensions/extensionChanges.js +131 -0
  5. package/dist/extensions/extensionCommand.js +25 -0
  6. package/dist/extensions/extensionDiscovery.js +141 -0
  7. package/dist/extensions/extensionRegistrar.js +82 -0
  8. package/dist/extensions/extensions.js +33 -0
  9. package/dist/groups/builtinGroups.js +60 -0
  10. package/dist/groups/defs/3dTools.js +17 -0
  11. package/dist/groups/defs/animationAuthoring.js +16 -0
  12. package/dist/groups/defs/assetOps.js +6 -0
  13. package/dist/groups/defs/audio.js +6 -0
  14. package/dist/groups/defs/classdb.js +6 -0
  15. package/dist/groups/defs/cleanup.js +6 -0
  16. package/dist/groups/defs/debugger.js +6 -0
  17. package/dist/groups/defs/editorAdvanced.js +16 -0
  18. package/dist/groups/defs/inputMap.js +6 -0
  19. package/dist/groups/defs/layerNaming.js +6 -0
  20. package/dist/groups/defs/lspCodeAnalysis.js +23 -0
  21. package/dist/groups/defs/lspCodeNavigation.js +15 -0
  22. package/dist/groups/defs/navigation.js +17 -0
  23. package/dist/groups/defs/particles.js +21 -0
  24. package/dist/groups/defs/pathEditing.js +22 -0
  25. package/dist/groups/defs/placeholders.js +28 -0
  26. package/dist/groups/defs/procedural.js +6 -0
  27. package/dist/groups/defs/resourceIo.js +6 -0
  28. package/dist/groups/defs/runtimeAdvanced.js +17 -0
  29. package/dist/groups/defs/sceneAdvanced.js +6 -0
  30. package/dist/groups/defs/sceneInheritance.js +6 -0
  31. package/dist/groups/defs/signals.js +6 -0
  32. package/dist/groups/defs/spriteframes.js +16 -0
  33. package/dist/groups/defs/theme.js +6 -0
  34. package/dist/groups/defs/tilemap.js +6 -0
  35. package/dist/groups/defs/tileset.js +22 -0
  36. package/dist/groups/defs/tilesetEdit.js +21 -0
  37. package/dist/groups/defs/userData.js +6 -0
  38. package/dist/groups/extensionGroups.js +182 -0
  39. package/dist/groups/groupActivation.js +210 -0
  40. package/dist/groups/groupCatalogue.js +52 -0
  41. package/dist/groups/groupMatch.js +182 -0
  42. package/dist/groups/groupResult.js +16 -0
  43. package/dist/groups/groupState.js +15 -0
  44. package/dist/groups/groupToolHandlers.js +103 -0
  45. package/dist/groups/groupTypes.js +10 -0
  46. package/dist/groups/groups.js +205 -0
  47. package/dist/index.js +144 -0
  48. package/dist/lsp/lspClient.js +509 -0
  49. package/dist/lsp/lspLabels.js +105 -0
  50. package/dist/lsp/lspProjectScan.js +156 -0
  51. package/dist/lsp/lspSession.js +139 -0
  52. package/dist/lsp/lspStatusReporter.js +76 -0
  53. package/dist/lsp/lspUri.js +68 -0
  54. package/dist/mcp/prompts.js +59 -0
  55. package/dist/mcp/resources.js +113 -0
  56. package/dist/mcp/roots.js +30 -0
  57. package/dist/registration/catalogue.js +123 -0
  58. package/dist/registration/extensionCollision.js +38 -0
  59. package/dist/registration/operations.js +67 -0
  60. package/dist/registration/screenshotResponse.js +71 -0
  61. package/dist/registration/toolDispatch.js +76 -0
  62. package/dist/registration/toolMeta.js +104 -0
  63. package/dist/registration/toolRefs.js +48 -0
  64. package/dist/registration/toolRegistry.js +211 -0
  65. package/dist/registry.js +291 -0
  66. package/dist/registryLiveness.js +113 -0
  67. package/dist/security/pathGuard.js +97 -0
  68. package/dist/security/profiles.js +104 -0
  69. package/dist/security/untrusted.js +23 -0
  70. package/dist/shared/errorContract.js +154 -0
  71. package/dist/shared/errors.js +21 -0
  72. package/dist/shared/pagination.js +135 -0
  73. package/dist/shared/schemaCoercion.js +173 -0
  74. package/dist/shared/stableJson.js +27 -0
  75. package/dist/shared/types.js +1 -0
  76. package/dist/shared/version.js +82 -0
  77. package/dist/startup/cliArgs.js +103 -0
  78. package/dist/startup/configReload.js +57 -0
  79. package/dist/startup/hooks.js +89 -0
  80. package/dist/startup/lifecycle.js +22 -0
  81. package/dist/startup/portConfig.js +127 -0
  82. package/dist/startup/reconcile.js +81 -0
  83. package/dist/startup/registrars.js +59 -0
  84. package/dist/startup/serverMode.js +19 -0
  85. package/dist/startup/startupEnv.js +142 -0
  86. package/dist/tools/animation.js +88 -0
  87. package/dist/tools/asset.js +58 -0
  88. package/dist/tools/assetWrite.js +16 -0
  89. package/dist/tools/audio.js +40 -0
  90. package/dist/tools/classdb.js +43 -0
  91. package/dist/tools/collision.js +23 -0
  92. package/dist/tools/debug.js +46 -0
  93. package/dist/tools/diff.js +18 -0
  94. package/dist/tools/editor.js +222 -0
  95. package/dist/tools/file.js +38 -0
  96. package/dist/tools/folder.js +27 -0
  97. package/dist/tools/inputMap.js +54 -0
  98. package/dist/tools/layerNames.js +29 -0
  99. package/dist/tools/lsp.js +524 -0
  100. package/dist/tools/navigation.js +24 -0
  101. package/dist/tools/node.js +145 -0
  102. package/dist/tools/nodeManagement.js +82 -0
  103. package/dist/tools/particles.js +83 -0
  104. package/dist/tools/path.js +30 -0
  105. package/dist/tools/playtest.js +99 -0
  106. package/dist/tools/procedural.js +101 -0
  107. package/dist/tools/resource.js +47 -0
  108. package/dist/tools/runtime.js +338 -0
  109. package/dist/tools/save.js +64 -0
  110. package/dist/tools/scene.js +102 -0
  111. package/dist/tools/sceneInheritance.js +19 -0
  112. package/dist/tools/sceneQuery.js +38 -0
  113. package/dist/tools/script.js +75 -0
  114. package/dist/tools/signals.js +53 -0
  115. package/dist/tools/sound.js +31 -0
  116. package/dist/tools/spatial.js +41 -0
  117. package/dist/tools/spriteframes.js +94 -0
  118. package/dist/tools/texture.js +35 -0
  119. package/dist/tools/theme.js +35 -0
  120. package/dist/tools/threeD.js +116 -0
  121. package/dist/tools/tilemap.js +51 -0
  122. package/dist/tools/tileset.js +226 -0
  123. package/dist/transport/authHandshake.js +43 -0
  124. package/dist/transport/bridge.js +225 -0
  125. package/dist/transport/channel.js +352 -0
  126. package/dist/transport/heartbeat.js +54 -0
  127. package/dist/transport/runtimeConnection.js +239 -0
  128. package/dist/transport/tokenPath.js +98 -0
  129. package/package.json +97 -4
@@ -0,0 +1,524 @@
1
+ /**
2
+ * LSP tool definitions — GDScript language intelligence via Godot's
3
+ * built-in language server. All tools are read-only and group-only
4
+ * (lazy-loaded via discover_tools), split across the lsp_code_analysis and
5
+ * lsp_code_navigation groups.
6
+ */
7
+ import { z } from "zod";
8
+ import { toolError } from "../shared/errorContract.js";
9
+ import { coercedBoolean } from "../shared/schemaCoercion.js";
10
+ import { untrustedWrap } from "../security/untrusted.js";
11
+ import { fileUriToRes } from "../lsp/lspUri.js";
12
+ import { formatDiagnostic, completionKindLabel, formatSymbol } from "../lsp/lspLabels.js";
13
+ import { withLspDoc, ensureLsp, openDocInLsp } from "../lsp/lspSession.js";
14
+ import { enumerateGdFiles, aggregateScan, filterBySeverity } from "../lsp/lspProjectScan.js";
15
+ // Re-export the session-layer symbols that external modules import from here,
16
+ // so their import paths stay stable.
17
+ export { setLspStatusReporter, lspConnectFailureHint } from "../lsp/lspSession.js";
18
+ // ── Tool definitions ──────────────────────────────────────────────────
19
+ // ── Primary group: lsp_code_analysis ─────────────────────────────────
20
+ // The tools used most frequently during a write→validate→explore cycle,
21
+ // plus the project-wide compile check.
22
+ export const lspAnalysisTools = [
23
+ {
24
+ name: "lsp_diagnostics",
25
+ method: "lsp.diagnostics",
26
+ description: "Rich GDScript diagnostics with column positions and severity (Error/Warning/Info/Hint). Needs editor running. " +
27
+ "Call editor_refresh first if files were just created.",
28
+ inputSchema: {
29
+ file_path: z
30
+ .string()
31
+ .describe("Godot resource path (must start with res://, e.g. res://scripts/player.gd). Only .gd and .gdshader — not .cs"),
32
+ },
33
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
34
+ successHint: "For quick validation use script_check. For runtime errors use editor_get_console.",
35
+ },
36
+ {
37
+ name: "lsp_symbols",
38
+ method: "lsp.symbols",
39
+ description: "List all symbols (functions, variables, classes, signals) in a .gd/.gdshader file. Structured tree — cheaper than reading full source.",
40
+ inputSchema: {
41
+ file_path: z
42
+ .string()
43
+ .describe("Godot resource path (must start with res://, e.g. res://scripts/player.gd). Only .gd and .gdshader — not .cs"),
44
+ },
45
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
46
+ },
47
+ {
48
+ name: "lsp_hover",
49
+ method: "lsp.hover",
50
+ description: "Get type signature and docs for one symbol at a specific position. Use for targeted type checks, not bulk exploration.",
51
+ inputSchema: {
52
+ file_path: z
53
+ .string()
54
+ .describe("Godot resource path (must start with res://, e.g. res://scripts/player.gd). Only .gd and .gdshader — not .cs"),
55
+ line: z.coerce.number().int().min(0).describe("Zero-based line number"),
56
+ column: z.coerce.number().int().min(0).describe("Zero-based column number"),
57
+ },
58
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
59
+ },
60
+ {
61
+ name: "lsp_project_diagnostics",
62
+ method: "lsp.project_diagnostics",
63
+ // Cost side-effect (not in the wire description to stay under 200 chars):
64
+ // on Godot 4.2–4.4 each didOpen also calls EditorFileSystem.update_file
65
+ // (harmless metadata churn; gone by 4.5). The scan opens every .gd, waits
66
+ // for its diagnostics, then closes it — chunked so the main-thread LSP
67
+ // parser is not swamped.
68
+ description: "Compile-checks every .gd in the project via the LSP — a guaranteed whole-project compile check. " +
69
+ "EXPENSIVE (~30s/100+ files; editor may hitch). Use sparingly.",
70
+ inputSchema: {
71
+ include_addons: coercedBoolean()
72
+ .optional()
73
+ .describe("Also scan res://addons/ (default false; needed when the project's scripts live under addons/)."),
74
+ include_warnings: coercedBoolean()
75
+ .optional()
76
+ .describe("Count Warning/Info/Hint diagnostics too (default false = errors only)."),
77
+ },
78
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
79
+ successHint: "Empty files_with_diagnostics = project compiles. For one file use lsp_diagnostics; for runtime errors use debugger_get_log.",
80
+ },
81
+ ];
82
+ // ── Secondary group: lsp_code_navigation ──────────────────────────────
83
+ // Situational tools for cross-file investigation: finding definitions,
84
+ // references, and available completions. Use sparingly — each call
85
+ // targets a single position.
86
+ export const lspNavigationTools = [
87
+ {
88
+ name: "lsp_completion",
89
+ method: "lsp.completion",
90
+ description: "Completions at a position. Use limit=5 for targeted queries to save tokens. Only call when you need to discover available API.",
91
+ inputSchema: {
92
+ file_path: z
93
+ .string()
94
+ .describe("Godot resource path (must start with res://, e.g. res://scripts/player.gd). Only .gd and .gdshader — not .cs"),
95
+ line: z.coerce.number().int().min(0).describe("Zero-based line number"),
96
+ column: z.coerce.number().int().min(0).describe("Zero-based column number"),
97
+ limit: z.coerce.number().int().min(1).max(50).default(10).describe("Max items to return (default 10)"),
98
+ },
99
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
100
+ },
101
+ {
102
+ name: "lsp_definition",
103
+ method: "lsp.definition",
104
+ description: "Go to definition: file + line where a symbol is defined. One position per call — use only when you need the source location.",
105
+ inputSchema: {
106
+ file_path: z
107
+ .string()
108
+ .describe("Godot resource path (must start with res://, e.g. res://scripts/player.gd). Only .gd and .gdshader — not .cs"),
109
+ line: z.coerce.number().int().min(0).describe("Zero-based line number"),
110
+ column: z.coerce.number().int().min(0).describe("Zero-based column number"),
111
+ },
112
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
113
+ },
114
+ {
115
+ name: "lsp_references",
116
+ method: "lsp.references",
117
+ description: "Find all references to a symbol across the project. Use before renaming/removing to assess impact. One symbol per call.",
118
+ inputSchema: {
119
+ file_path: z
120
+ .string()
121
+ .describe("Godot resource path (must start with res://, e.g. res://scripts/player.gd). Only .gd and .gdshader — not .cs"),
122
+ line: z.coerce.number().int().min(0).describe("Zero-based line number"),
123
+ column: z.coerce.number().int().min(0).describe("Zero-based column number"),
124
+ },
125
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
126
+ },
127
+ ];
128
+ /** All LSP tools combined (for catalogue/static checks). */
129
+ export const lspTools = [...lspAnalysisTools, ...lspNavigationTools];
130
+ // ── Handler factory ───────────────────────────────────────────────────
131
+ /**
132
+ * Create a handler for an LSP tool. Each handler:
133
+ * 1. Validates input (rejects .cs files)
134
+ * 2. Ensures LSP connection
135
+ * 3. Reads file content and opens in LSP
136
+ * 4. Sends the appropriate LSP request
137
+ * 5. Converts URIs back to res:// paths
138
+ */
139
+ export function createLspHandler(toolName, projectPath) {
140
+ switch (toolName) {
141
+ case "lsp_diagnostics":
142
+ return (input) => handleDiagnostics(input, projectPath);
143
+ case "lsp_hover":
144
+ return (input) => handleHover(input, projectPath);
145
+ case "lsp_completion":
146
+ return (input) => handleCompletion(input, projectPath);
147
+ case "lsp_definition":
148
+ return (input) => handleDefinition(input, projectPath);
149
+ case "lsp_symbols":
150
+ return (input) => handleSymbols(input, projectPath);
151
+ case "lsp_references":
152
+ return (input) => handleReferences(input, projectPath);
153
+ case "lsp_project_diagnostics":
154
+ return (input) => handleProjectDiagnostics(input, projectPath);
155
+ default:
156
+ return async () => toolError("INTERNAL", `Unknown LSP tool: ${toolName}`);
157
+ }
158
+ }
159
+ // ── Individual handlers ──────────────────────────────────────────────
160
+ // Shader files get no real diagnostics from the Godot LSP — it analyzes GDScript
161
+ // only, and Godot exposes no offline shader-compile API. Every diagnostic the LSP
162
+ // would emit on a .gdshader/.gdshaderinc is a bogus GDScript-parse artifact (the
163
+ // 4.6/4.7 languageId:"gdshader" workaround suppresses them, but 4.2-4.5 ignore
164
+ // languageId). Short-circuit BEFORE touching the LSP so the empty result is
165
+ // uniform across 4.2-4.7 and needs no editor connection.
166
+ const SHADER_DIAGNOSTICS_NOTE = "lsp_diagnostics does not validate shader files — Godot's LSP analyzes GDScript only. " +
167
+ "Real shader errors surface when the editor imports/compiles the shader (open it, or run the game); " +
168
+ "read them with editor_get_console (level_filter:['error']).";
169
+ async function handleDiagnostics(input, projectPath) {
170
+ const { file_path } = input;
171
+ if (file_path.endsWith(".gdshader") || file_path.endsWith(".gdshaderinc")) {
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: JSON.stringify({
177
+ success: true,
178
+ file_path,
179
+ diagnostics: [],
180
+ count: 0,
181
+ note: SHADER_DIAGNOSTICS_NOTE,
182
+ }),
183
+ },
184
+ ],
185
+ };
186
+ }
187
+ const doc = await withLspDoc(file_path, projectPath);
188
+ if ("content" in doc)
189
+ return doc;
190
+ const { client, uri } = doc;
191
+ // Wait for diagnostics notification from the LSP.
192
+ const diagnostics = await client.waitForDiagnostics(uri);
193
+ // undefined = no notification arrived within the wait (status unknown), which
194
+ // must not be reported as an empty (clean) result. A received [] IS clean.
195
+ if (diagnostics === undefined) {
196
+ return {
197
+ content: [
198
+ {
199
+ type: "text",
200
+ text: JSON.stringify({
201
+ success: true,
202
+ file_path,
203
+ diagnostics: [],
204
+ count: 0,
205
+ note: "no diagnostics notification within 5 s — status unknown, retry",
206
+ }),
207
+ },
208
+ ],
209
+ };
210
+ }
211
+ const formatted = diagnostics.map(formatDiagnostic);
212
+ return {
213
+ content: [
214
+ {
215
+ type: "text",
216
+ text: JSON.stringify({ success: true, file_path, diagnostics: formatted, count: formatted.length }),
217
+ },
218
+ ],
219
+ };
220
+ }
221
+ async function handleHover(input, projectPath) {
222
+ const { file_path, line, column } = input;
223
+ const doc = await withLspDoc(file_path, projectPath);
224
+ if ("content" in doc)
225
+ return doc;
226
+ const { client, uri } = doc;
227
+ const result = (await client.sendRequest("textDocument/hover", {
228
+ textDocument: { uri },
229
+ position: { line, character: column },
230
+ }));
231
+ if (!result || !result.contents) {
232
+ return {
233
+ content: [
234
+ {
235
+ type: "text",
236
+ text: JSON.stringify({ success: true, file_path, position: { line, column }, contents: null }),
237
+ },
238
+ ],
239
+ };
240
+ }
241
+ // Extract hover text from LSP MarkupContent or string.
242
+ let hoverText;
243
+ if (typeof result.contents === "string") {
244
+ hoverText = result.contents;
245
+ }
246
+ else if (typeof result.contents.value === "string") {
247
+ hoverText = result.contents.value;
248
+ }
249
+ else if (Array.isArray(result.contents)) {
250
+ hoverText = result.contents
251
+ .map((c) => (typeof c === "string" ? c : (c.value ?? "")))
252
+ .join("\n");
253
+ }
254
+ else {
255
+ hoverText = JSON.stringify(result.contents);
256
+ }
257
+ // Hover markdown can embed file:// links to symbol definitions; convert the
258
+ // in-project ones to res:// for consistency with lsp_definition/references
259
+ // (fileUriToRes leaves out-of-project URIs untouched).
260
+ hoverText = hoverText.replace(/file:\/\/[^\s)<>"'`\]]+/g, (fileUri) => fileUriToRes(fileUri, projectPath));
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: JSON.stringify({
266
+ success: true,
267
+ file_path,
268
+ position: { line, column },
269
+ contents: untrustedWrap("hover", "godot-lsp", hoverText),
270
+ }),
271
+ },
272
+ ],
273
+ };
274
+ }
275
+ async function handleCompletion(input, projectPath) {
276
+ const { file_path, line, column, limit } = input;
277
+ const doc = await withLspDoc(file_path, projectPath);
278
+ if ("content" in doc)
279
+ return doc;
280
+ const { client, uri } = doc;
281
+ const result = (await client.sendRequest("textDocument/completion", {
282
+ textDocument: { uri },
283
+ position: { line, character: column },
284
+ }));
285
+ // LSP returns either CompletionList { items: [...] } or CompletionItem[].
286
+ let items;
287
+ if (Array.isArray(result)) {
288
+ items = result;
289
+ }
290
+ else if (result && Array.isArray(result.items)) {
291
+ items = result.items;
292
+ }
293
+ else {
294
+ items = [];
295
+ }
296
+ // Cap at limit and extract relevant fields.
297
+ const maxItems = limit ?? 10;
298
+ const completions = items.slice(0, maxItems).map((item) => {
299
+ const c = item;
300
+ return {
301
+ label: c.label ?? "",
302
+ kind: completionKindLabel(c.kind),
303
+ ...(c.detail ? { detail: c.detail } : {}),
304
+ ...(c.documentation
305
+ ? {
306
+ documentation: typeof c.documentation === "string" ? c.documentation : c.documentation.value,
307
+ }
308
+ : {}),
309
+ };
310
+ });
311
+ return {
312
+ content: [
313
+ {
314
+ type: "text",
315
+ text: JSON.stringify({
316
+ success: true,
317
+ file_path,
318
+ position: { line, column },
319
+ completions,
320
+ count: completions.length,
321
+ total: items.length,
322
+ }),
323
+ },
324
+ ],
325
+ };
326
+ }
327
+ async function handleDefinition(input, projectPath) {
328
+ const { file_path, line, column } = input;
329
+ const doc = await withLspDoc(file_path, projectPath);
330
+ if ("content" in doc)
331
+ return doc;
332
+ const { client, uri } = doc;
333
+ const result = (await client.sendRequest("textDocument/definition", {
334
+ textDocument: { uri },
335
+ position: { line, character: column },
336
+ }));
337
+ if (!result) {
338
+ return {
339
+ content: [
340
+ {
341
+ type: "text",
342
+ text: JSON.stringify({ success: true, file_path, position: { line, column }, definition: null }),
343
+ },
344
+ ],
345
+ };
346
+ }
347
+ // LSP returns Location | Location[] | LocationLink[].
348
+ const locations = Array.isArray(result) ? result : [result];
349
+ const definitions = locations.map((loc) => {
350
+ const l = loc;
351
+ const locUri = l.uri ?? l.targetUri ?? "";
352
+ const range = l.range ?? l.targetRange;
353
+ return {
354
+ file_path: fileUriToRes(locUri, projectPath),
355
+ line: (range?.start?.line ?? 0) + 1,
356
+ column: (range?.start?.character ?? 0) + 1,
357
+ };
358
+ });
359
+ return {
360
+ content: [
361
+ {
362
+ type: "text",
363
+ text: JSON.stringify({
364
+ success: true,
365
+ file_path,
366
+ position: { line, column },
367
+ definition: definitions.length === 1 ? definitions[0] : definitions,
368
+ }),
369
+ },
370
+ ],
371
+ };
372
+ }
373
+ async function handleSymbols(input, projectPath) {
374
+ const { file_path } = input;
375
+ const doc = await withLspDoc(file_path, projectPath);
376
+ if ("content" in doc)
377
+ return doc;
378
+ const { client, uri } = doc;
379
+ const result = (await client.sendRequest("textDocument/documentSymbol", {
380
+ textDocument: { uri },
381
+ }));
382
+ if (!result || !Array.isArray(result)) {
383
+ return {
384
+ content: [{ type: "text", text: JSON.stringify({ success: true, file_path, symbols: [], count: 0 }) }],
385
+ };
386
+ }
387
+ const symbols = result.map(formatSymbol);
388
+ return {
389
+ content: [
390
+ {
391
+ type: "text",
392
+ text: JSON.stringify({ success: true, file_path, symbols, count: symbols.length }),
393
+ },
394
+ ],
395
+ };
396
+ }
397
+ async function handleReferences(input, projectPath) {
398
+ const { file_path, line, column } = input;
399
+ const doc = await withLspDoc(file_path, projectPath);
400
+ if ("content" in doc)
401
+ return doc;
402
+ const { client, uri } = doc;
403
+ const result = (await client.sendRequest("textDocument/references", {
404
+ textDocument: { uri },
405
+ position: { line, character: column },
406
+ context: { includeDeclaration: true },
407
+ }));
408
+ if (!result || !Array.isArray(result)) {
409
+ return {
410
+ content: [
411
+ {
412
+ type: "text",
413
+ text: JSON.stringify({ success: true, file_path, position: { line, column }, references: [], count: 0 }),
414
+ },
415
+ ],
416
+ };
417
+ }
418
+ const references = result.map((loc) => {
419
+ const l = loc;
420
+ return {
421
+ file_path: fileUriToRes(l.uri ?? "", projectPath),
422
+ line: (l.range?.start?.line ?? 0) + 1,
423
+ column: (l.range?.start?.character ?? 0) + 1,
424
+ };
425
+ });
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: JSON.stringify({
431
+ success: true,
432
+ file_path,
433
+ position: { line, column },
434
+ references,
435
+ count: references.length,
436
+ }),
437
+ },
438
+ ],
439
+ };
440
+ }
441
+ // ── Project-wide scan ────────────────────────────────────────────────
442
+ // Open ~20 files per chunk, then wait for each; keeps the editor's main-thread
443
+ // GDScript parser (use_thread=false, 4.2→4.7) from being swamped by a flat
444
+ // fan-out that would structurally under-budget late files.
445
+ const PROJECT_SCAN_CHUNK_SIZE = 20;
446
+ // Per-file diagnostics wait. A clean file publishes an empty-diagnostics
447
+ // notification promptly; this budget covers a slow first parse.
448
+ const PROJECT_SCAN_WAIT_MS = 10_000;
449
+ /**
450
+ * Scan every project `.gd` for compile diagnostics. Enumerates the files off
451
+ * disk, then fans the singleton LSP client over them in chunks — open the
452
+ * chunk, wait for each file's diagnostics, close the chunk — never closing a
453
+ * URI with a pending wait. A file that never publishes within the wait is
454
+ * classified timed_out (never clean); a whole chunk going mute means the editor
455
+ * is wedged and aborts the scan with LSP_UNAVAILABLE.
456
+ */
457
+ async function handleProjectDiagnostics(input, projectPath) {
458
+ const { include_addons, include_warnings } = input;
459
+ const includeAddons = include_addons ?? false;
460
+ const includeWarnings = include_warnings ?? false;
461
+ const files = await enumerateGdFiles(projectPath, { includeAddons });
462
+ if (files.length === 0) {
463
+ return jsonResult(aggregateScan([], PROJECT_SCAN_WAIT_MS / 1000));
464
+ }
465
+ const clientOrErr = await ensureLsp(projectPath);
466
+ if ("content" in clientOrErr)
467
+ return clientOrErr;
468
+ const client = clientOrErr;
469
+ const results = [];
470
+ for (let i = 0; i < files.length; i += PROJECT_SCAN_CHUNK_SIZE) {
471
+ const chunk = files.slice(i, i + PROJECT_SCAN_CHUNK_SIZE);
472
+ const chunkResult = await scanChunk(client, chunk, projectPath, includeWarnings);
473
+ if ("content" in chunkResult)
474
+ return chunkResult; // Early-abort (LSP mute).
475
+ results.push(...chunkResult.results);
476
+ }
477
+ return jsonResult(aggregateScan(results, PROJECT_SCAN_WAIT_MS / 1000));
478
+ }
479
+ /**
480
+ * Scan one chunk: open every file, await each file's diagnostics, then close
481
+ * every opened file. Returns the per-file classifications, or a mute-editor
482
+ * tool error when every opened file in the chunk timed out.
483
+ */
484
+ async function scanChunk(client, chunk, projectPath, includeWarnings) {
485
+ // Open first, collecting the URIs to wait on (a read failure is not opened).
486
+ const opened = [];
487
+ const results = [];
488
+ for (const filePath of chunk) {
489
+ const openResult = await openDocInLsp(client, filePath, projectPath);
490
+ if ("content" in openResult) {
491
+ // The only non-URI outcome is READ_FAILED (file vanished after the walk).
492
+ results.push({ filePath, kind: "read_failed" });
493
+ continue;
494
+ }
495
+ opened.push({ filePath, uri: openResult.uri });
496
+ }
497
+ // Wait for each opened file's diagnostics before closing anything — closing a
498
+ // URI with a pending wait would drop its late publish.
499
+ const waited = await Promise.all(opened.map(async ({ filePath, uri }) => ({
500
+ filePath,
501
+ diagnostics: await client.waitForDiagnostics(uri, PROJECT_SCAN_WAIT_MS),
502
+ })));
503
+ for (const { uri } of opened) {
504
+ await client.closeDocument(uri);
505
+ }
506
+ // A chunk whose every opened file went mute means the editor is wedged (not a
507
+ // project full of huge files) — abort rather than mislabel the rest.
508
+ if (opened.length > 0 && waited.every((w) => w.diagnostics === undefined)) {
509
+ return toolError("LSP_UNAVAILABLE", `Connected to the GDScript LSP but received no diagnostics within ${PROJECT_SCAN_WAIT_MS / 1000}s across ${opened.length} files — the editor may be busy or wedged. Retry shortly.`);
510
+ }
511
+ for (const { filePath, diagnostics } of waited) {
512
+ if (diagnostics === undefined) {
513
+ results.push({ filePath, kind: "timed_out" });
514
+ continue;
515
+ }
516
+ const filtered = filterBySeverity(diagnostics, includeWarnings);
517
+ results.push(filtered.length === 0 ? { filePath, kind: "clean" } : { filePath, kind: "diagnostics", diagnostics: filtered });
518
+ }
519
+ return { results };
520
+ }
521
+ /** Wrap a JSON-serializable payload as the tool's text result. */
522
+ function jsonResult(payload) {
523
+ return { content: [{ type: "text", text: JSON.stringify(payload) }] };
524
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ export const navigationTools = [
3
+ {
4
+ name: "navigation_edit",
5
+ method: "navigation.edit_polygon",
6
+ description: "Edit NavigationRegion2D polygon outlines: set all outlines, add/remove individual outlines, clear, or bake the navigation mesh. Required for AI pathfinding setup.",
7
+ inputSchema: {
8
+ node_path: z.string().describe("Path to NavigationRegion2D node"),
9
+ action: z.enum(["set", "add_outline", "remove_outline", "clear", "bake"]).describe("Polygon operation"),
10
+ outlines: z
11
+ .array(z.array(z.object({ x: z.number(), y: z.number() })))
12
+ .optional()
13
+ .describe("For 'set': array of outline arrays (each outline = array of {x,y} points)"),
14
+ outline: z
15
+ .array(z.object({ x: z.number(), y: z.number() }))
16
+ .optional()
17
+ .describe("For 'add_outline': single outline as array of {x,y} points"),
18
+ index: z.number().int().optional().describe("For 'remove_outline': outline index"),
19
+ },
20
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true },
21
+ operationParam: "action",
22
+ successHint: "After defining polygons, call with action 'bake' to generate the navigation mesh.",
23
+ },
24
+ ];