@npgamedev/godot-mcp-server 0.0.1 → 1.0.1

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 +89 -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 +59 -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 +355 -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 +98 -4
@@ -0,0 +1,21 @@
1
+ export const particlesGroup = {
2
+ name: "particles",
3
+ description: "Create and configure GPU particle systems for visual effects",
4
+ tools: ["particles_create"],
5
+ keywords: [
6
+ "particle",
7
+ "particles",
8
+ "gpu particles",
9
+ "vfx",
10
+ "visual effect",
11
+ "effects",
12
+ "fire",
13
+ "smoke",
14
+ "sparks",
15
+ "rain",
16
+ "snow",
17
+ "explosion",
18
+ "emitter",
19
+ "particle system",
20
+ ],
21
+ };
@@ -0,0 +1,22 @@
1
+ export const pathEditingGroup = {
2
+ name: "path_editing",
3
+ description: "Edit Path2D curves and generate collision shapes from sprite textures",
4
+ tools: ["path2d_edit_curve", "collision_from_texture"],
5
+ keywords: [
6
+ "path",
7
+ "path2d",
8
+ "curve",
9
+ "bezier",
10
+ "spline",
11
+ "follow path",
12
+ "pathfollow",
13
+ "curve2d",
14
+ "2d",
15
+ "collision",
16
+ "collision polygon",
17
+ "sprite",
18
+ "bitmap",
19
+ "alpha",
20
+ "shape from texture",
21
+ ],
22
+ };
@@ -0,0 +1,28 @@
1
+ export const placeholdersGroup = {
2
+ name: "placeholders",
3
+ description: "Generate placeholder/prototype assets procedurally — textures (shapes, patterns, labels) and sound effects (tones, noise). No art or network needed.",
4
+ tools: ["texture_generate", "sound_generate"],
5
+ keywords: [
6
+ "placeholder",
7
+ "prototype",
8
+ "prototyping",
9
+ "stand-in",
10
+ "mock",
11
+ "generate",
12
+ "procedural",
13
+ "texture",
14
+ "sprite",
15
+ "image",
16
+ "icon",
17
+ "png",
18
+ "sound",
19
+ "sfx",
20
+ "audio",
21
+ "tone",
22
+ "beep",
23
+ "noise",
24
+ "wav",
25
+ "art",
26
+ "asset",
27
+ ],
28
+ };
@@ -0,0 +1,6 @@
1
+ export const proceduralGroup = {
2
+ name: "procedural",
3
+ description: "Edit gradients, curves, and FastNoiseLite resources for procedural generation",
4
+ tools: ["procedural_edit_gradient", "procedural_edit_curve", "procedural_edit_noise"],
5
+ keywords: ["procedural", "generate", "gradient", "noise", "curve", "resource create", "fastnoiselite", "easing"],
6
+ };
@@ -0,0 +1,6 @@
1
+ export const resourceIoGroup = {
2
+ name: "resource_io",
3
+ description: "Load and write Godot resources (.tres/.res) programmatically",
4
+ tools: ["resource_load", "resource_write"],
5
+ keywords: ["resource", "load", "write", "save resource", "tres", "res"],
6
+ };
@@ -0,0 +1,17 @@
1
+ export const runtimeAdvancedGroup = {
2
+ name: "runtime_advanced",
3
+ description: "Inspect live node state, set node properties, and control AnimationPlayer during playtests",
4
+ tools: ["runtime_get_node_state", "runtime_set_property", "animation_player_control"],
5
+ keywords: [
6
+ "runtime",
7
+ "node state",
8
+ "set property",
9
+ "runtime property",
10
+ "animation playback",
11
+ "animationplayer",
12
+ "play animation",
13
+ "stop animation",
14
+ "animation control",
15
+ "inspect node",
16
+ ],
17
+ };
@@ -0,0 +1,6 @@
1
+ export const sceneAdvancedGroup = {
2
+ name: "scene_advanced",
3
+ description: "Diff scenes and batch-instantiate nodes from packed scenes",
4
+ tools: ["scene_diff", "scene_instantiate"],
5
+ keywords: ["instantiate", "instance", "scene diff", "compare", "prefab", "spawn", "batch instantiate"],
6
+ };
@@ -0,0 +1,6 @@
1
+ export const sceneInheritanceGroup = {
2
+ name: "scene_inheritance",
3
+ description: "Create inherited scenes (variants) from base scenes",
4
+ tools: ["scene_create_inherited"],
5
+ keywords: ["inheritance", "inherited scene", "prefab", "variant", "base scene", "scene extend", "inherit"],
6
+ };
@@ -0,0 +1,6 @@
1
+ export const signalsGroup = {
2
+ name: "signals",
3
+ description: "Emit signals on scene nodes at editor-time or runtime",
4
+ tools: ["signal_emit"],
5
+ keywords: ["signal", "emit", "observer", "event", "handler", "callback"],
6
+ };
@@ -0,0 +1,16 @@
1
+ export const spriteframesGroup = {
2
+ name: "spriteframes",
3
+ description: "List, create, and edit SpriteFrames animations and import from spritesheets",
4
+ tools: ["spriteframes_create", "spriteframes_edit", "spriteframes_from_spritesheet"],
5
+ keywords: [
6
+ "sprite",
7
+ "spriteframes",
8
+ "animated sprite",
9
+ "frame",
10
+ "flipbook",
11
+ "2d animation",
12
+ "spritesheet",
13
+ "atlas",
14
+ "2d",
15
+ ],
16
+ };
@@ -0,0 +1,6 @@
1
+ export const themeGroup = {
2
+ name: "theme",
3
+ description: "Edit UI theme overrides: styleboxes, fonts, colors, and constants",
4
+ tools: ["theme_edit"],
5
+ keywords: ["theme", "style", "stylebox", "font", "color", "ui style", "control theme"],
6
+ };
@@ -0,0 +1,6 @@
1
+ export const tilemapGroup = {
2
+ name: "tilemap",
3
+ description: "Read and paint cells on TileMap/TileMapLayer nodes — cell queries, bulk fills, and region operations",
4
+ tools: ["tilemap_read_cells", "tilemap_set_cells"],
5
+ keywords: ["tilemap", "tile", "grid", "cell", "read cells", "paint cells", "2d"],
6
+ };
@@ -0,0 +1,22 @@
1
+ export const tilesetGroup = {
2
+ name: "tileset",
3
+ description: "Create TileSet resources, add atlas sources, configure layers, and manage tile alternatives",
4
+ tools: [
5
+ "tileset_create",
6
+ "tileset_add_source",
7
+ "tileset_remove_source",
8
+ "tileset_add_alternative",
9
+ "tileset_remove_alternative",
10
+ "tileset_setup_layers",
11
+ ],
12
+ keywords: [
13
+ "tileset",
14
+ "atlas",
15
+ "tile source",
16
+ "tile layer",
17
+ "terrain set",
18
+ "tile alternative",
19
+ "tile variant",
20
+ "create tileset",
21
+ ],
22
+ };
@@ -0,0 +1,21 @@
1
+ export const tilesetEditGroup = {
2
+ name: "tileset_edit",
3
+ description: "Edit per-tile properties: physics, terrain, navigation, visuals, and custom data",
4
+ tools: [
5
+ "tileset_edit_physics",
6
+ "tileset_edit_terrain",
7
+ "tileset_edit_navigation",
8
+ "tileset_edit_visuals",
9
+ "tileset_edit_custom_data",
10
+ ],
11
+ keywords: [
12
+ "tileset collision",
13
+ "tile physics",
14
+ "tile terrain",
15
+ "tile navigation",
16
+ "tile occlusion",
17
+ "tile animation",
18
+ "tile custom data",
19
+ "peering bits",
20
+ ],
21
+ };
@@ -0,0 +1,6 @@
1
+ export const userDataGroup = {
2
+ name: "user_data",
3
+ description: "Read, write, delete, and list user:// save files",
4
+ tools: ["save_read", "save_write", "save_delete", "save_list"],
5
+ keywords: ["save", "save file", "user data", "persistence", "save game", "load game", "savegame"],
6
+ };
@@ -0,0 +1,182 @@
1
+ import { activatedResult, alreadyLoadedResult, availableResult, readOnlyEmptyResult } from "./groupResult.js";
2
+ import { registerToolWrapped } from "../registration/toolRegistry.js";
3
+ import { callAndWrap } from "../registration/toolDispatch.js";
4
+ import { removeToolByName } from "../registration/toolRefs.js";
5
+ import { extensionNameCollides } from "../registration/extensionCollision.js";
6
+ import { isAllowedInReadOnly, isExcludedByReadOnly } from "../security/profiles.js";
7
+ // ── Registry state (private — read only through the accessors below) ──
8
+ const extensionGroups = new Map();
9
+ const loadedExtensionGroups = new Set();
10
+ // ── Mutators ─────────────────────────────────────────────────────────
11
+ /** Register a deferred extension group (called from discoverExtensions). Deduplicates by method name. */
12
+ export function addExtensionGroup(name, description, commands, keywords) {
13
+ const existing = extensionGroups.get(name);
14
+ if (existing) {
15
+ for (const cmd of commands) {
16
+ if (!existing.commands.some((c) => c.method === cmd.method)) {
17
+ existing.commands.push(cmd);
18
+ }
19
+ }
20
+ // Merge description if different.
21
+ if (description && description !== existing.description) {
22
+ existing.description = existing.description + "; " + description;
23
+ }
24
+ // Merge keywords without duplicates.
25
+ if (keywords) {
26
+ for (const kw of keywords) {
27
+ if (!existing.keywords.includes(kw))
28
+ existing.keywords.push(kw);
29
+ }
30
+ }
31
+ }
32
+ else {
33
+ extensionGroups.set(name, { name, description, keywords: keywords ?? [], commands });
34
+ }
35
+ }
36
+ /** Remove a single command from an extension group by method name. Returns true if found. */
37
+ export function removeExtensionCommand(method) {
38
+ for (const [name, group] of extensionGroups) {
39
+ const idx = group.commands.findIndex((c) => c.method === method);
40
+ if (idx >= 0) {
41
+ const toolName = group.commands[idx].toolName;
42
+ group.commands.splice(idx, 1);
43
+ removeToolByName(toolName);
44
+ // If no commands remain, remove the entire group.
45
+ if (group.commands.length === 0) {
46
+ extensionGroups.delete(name);
47
+ loadedExtensionGroups.delete(name);
48
+ }
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+ /** Remove an entire extension group by name. Unregisters all its tools. */
55
+ export function removeExtensionGroup(name) {
56
+ const group = extensionGroups.get(name);
57
+ if (!group)
58
+ return false;
59
+ for (const cmd of group.commands) {
60
+ removeToolByName(cmd.toolName);
61
+ }
62
+ extensionGroups.delete(name);
63
+ loadedExtensionGroups.delete(name);
64
+ return true;
65
+ }
66
+ /** Remove an ungrouped extension tool by its method-derived tool name. */
67
+ export function removeUngroupedExtensionTool(toolName) {
68
+ return removeToolByName(toolName);
69
+ }
70
+ /** Whether any extension groups exist (used to decide if refresh needed). */
71
+ export function hasExtensionGroups() {
72
+ return extensionGroups.size > 0;
73
+ }
74
+ // ── Registration ─────────────────────────────────────────────────────
75
+ /** Register an extension group's tools (called from discover_tools handler). */
76
+ export function registerExtGroupTools(server, bridge, group, readOnly = false) {
77
+ const registered = [];
78
+ for (const cmd of group.commands) {
79
+ if (isExcludedByReadOnly(readOnly, cmd.annotations))
80
+ continue;
81
+ // A grouped extension tool can only clash with a built-in or another group's
82
+ // tool at activation time (registration is deferred + order-dependent) — refuse
83
+ // it here so the incumbent is never overwritten.
84
+ if (extensionNameCollides(cmd.toolName))
85
+ continue;
86
+ registerToolWrapped(server, bridge, cmd.toolName, {
87
+ description: cmd.description,
88
+ inputSchema: cmd.inputSchema,
89
+ annotations: {
90
+ readOnlyHint: cmd.annotations.readOnlyHint ?? false,
91
+ destructiveHint: cmd.annotations.destructiveHint ?? false,
92
+ idempotentHint: cmd.annotations.idempotentHint ?? false,
93
+ },
94
+ }, (input, signal) => callAndWrap(bridge, cmd.method, input, { signal }));
95
+ registered.push(cmd.toolName);
96
+ }
97
+ return registered;
98
+ }
99
+ // ── Activate / report (CQS split: command + query) ───────────
100
+ /**
101
+ * Activate an extension group by name (the COMMAND half of the old fused
102
+ * activateOrReportExtGroup). Idempotent on an already-loaded group; keeps the
103
+ * read-only "don't waste a slot" guard. The caller dispatches on the activate
104
+ * flag (activate ? activateExtGroup : reportExtGroupStatus).
105
+ */
106
+ export function activateExtGroup(server, bridge, name, readOnly = false) {
107
+ const ext = extensionGroups.get(name);
108
+ if (!ext) {
109
+ return availableResult(name, [], `Unknown group: ${name}`);
110
+ }
111
+ const toolNames = readOnly
112
+ ? ext.commands.filter((c) => isAllowedInReadOnly(c.annotations)).map((c) => c.toolName)
113
+ : ext.commands.map((c) => c.toolName);
114
+ const tools = toolNames.map((t) => ({ name: t }));
115
+ if (loadedExtensionGroups.has(name)) {
116
+ return alreadyLoadedResult(name, tools, ext.description);
117
+ }
118
+ const registered = registerExtGroupTools(server, bridge, ext, readOnly);
119
+ // In read-only mode, if all tools were filtered out, don't waste a group slot.
120
+ if (readOnly && registered.length === 0) {
121
+ return readOnlyEmptyResult(name);
122
+ }
123
+ loadedExtensionGroups.add(name);
124
+ return activatedResult(name, registered, ext.description);
125
+ }
126
+ /** Report an extension group's status without mutating (the QUERY half). */
127
+ export function reportExtGroupStatus(name, readOnly = false) {
128
+ const ext = extensionGroups.get(name);
129
+ if (!ext)
130
+ return { name, status: "available", tools: [] };
131
+ // In read-only mode, filter the tool list to only read-only tools (mirrors the built-in reportGroupStatus,
132
+ // preserving the old fused activateOrReportExtGroup query behavior). Browse callers omit readOnly (unfiltered).
133
+ const toolNames = readOnly
134
+ ? ext.commands.filter((c) => isAllowedInReadOnly(c.annotations)).map((c) => c.toolName)
135
+ : ext.commands.map((c) => c.toolName);
136
+ const tools = toolNames.map((t) => ({ name: t }));
137
+ if (loadedExtensionGroups.has(name))
138
+ return alreadyLoadedResult(name, tools, ext.description);
139
+ return availableResult(name, tools, ext.description);
140
+ }
141
+ // ── Purpose accessors (the maps stay private) ──
142
+ /** Clear both registry maps (used by resetLoadedGroups). */
143
+ export function clearExtensionGroups() {
144
+ extensionGroups.clear();
145
+ loadedExtensionGroups.clear();
146
+ }
147
+ /**
148
+ * Unload a loaded extension group: unregister its command tools and clear its
149
+ * loaded flag. Returns true if the group was loaded (now deactivated), false
150
+ * otherwise — so deactivateGroups collects exactly the names it unloaded.
151
+ */
152
+ export function deactivateExtensionGroup(name) {
153
+ if (!loadedExtensionGroups.has(name))
154
+ return false;
155
+ const ext = extensionGroups.get(name);
156
+ if (ext) {
157
+ for (const cmd of ext.commands)
158
+ removeToolByName(cmd.toolName);
159
+ }
160
+ loadedExtensionGroups.delete(name);
161
+ return true;
162
+ }
163
+ /** Iterate the registered extension groups (name → def), in insertion order. */
164
+ export function extensionGroupEntries() {
165
+ return extensionGroups.entries();
166
+ }
167
+ /** Look up a registered extension group by name. */
168
+ export function getExtensionGroup(name) {
169
+ return extensionGroups.get(name);
170
+ }
171
+ /** Whether an extension group is currently loaded (its tools registered). */
172
+ export function isExtensionGroupLoaded(name) {
173
+ return loadedExtensionGroups.has(name);
174
+ }
175
+ /** How many extension groups are currently loaded. */
176
+ export function loadedExtensionGroupCount() {
177
+ return loadedExtensionGroups.size;
178
+ }
179
+ /** The names of the currently-loaded extension groups, in insertion order. */
180
+ export function loadedExtensionGroupNames() {
181
+ return [...loadedExtensionGroups];
182
+ }
@@ -0,0 +1,210 @@
1
+ import { GROUPS, allDefs } from "./groupCatalogue.js";
2
+ import { loadedGroups } from "./groupState.js";
3
+ import { activateExtGroup, reportExtGroupStatus, extensionGroupEntries, isExtensionGroupLoaded, loadedExtensionGroupNames, deactivateExtensionGroup, } from "./extensionGroups.js";
4
+ import { createGroupToolHandler } from "./groupToolHandlers.js";
5
+ import { isAllowedInReadOnly, isExcludedByReadOnly } from "../security/profiles.js";
6
+ import { isVersionCompatible } from "../shared/version.js";
7
+ import { registerToolWrapped } from "../registration/toolRegistry.js";
8
+ import { removeToolByName } from "../registration/toolRefs.js";
9
+ import { activatedResult, alreadyLoadedResult, availableResult, readOnlyEmptyResult } from "./groupResult.js";
10
+ // ── Version-gated visibility ─────────────────────────────────────────
11
+ // discover_tools' group summaries are built from the STATIC catalogue, but the
12
+ // registration gate installs only the tools the connected editor can serve
13
+ // (registerToolWrapped). These predicates re-apply that same gate to the
14
+ // advertise surface, so a below-gate editor never sees a tool that tools/list
15
+ // omits — the advertise surface matches the register surface.
16
+ /**
17
+ * Whether the connected editor can serve a version-gated built-in — the same
18
+ * predicate the registration gate applies. Unversioned tools always pass; a gated
19
+ * tool needs a known connected version within its [min, max] bounds. A null
20
+ * connected version mirrors registration's conservative refusal (skip the
21
+ * unverifiable), so the advertise surface can never over-claim a tool the register
22
+ * surface skipped.
23
+ */
24
+ function isToolVersionCompatible(bridge, def) {
25
+ if (def.godotMinVersion == null && def.godotMaxVersion == null)
26
+ return true;
27
+ const connected = bridge.getGodotVersion();
28
+ if (connected == null)
29
+ return false;
30
+ return isVersionCompatible(connected, def.godotMinVersion, def.godotMaxVersion);
31
+ }
32
+ /**
33
+ * Whether a built-in group tool belongs on the advertise surface: registrable for
34
+ * the connected editor version AND — in read-only mode — allowed by its
35
+ * annotations. The single predicate that keeps group summaries in lockstep with
36
+ * what registration installs.
37
+ */
38
+ function isToolVisible(bridge, def, readOnly) {
39
+ if (readOnly && !isAllowedInReadOnly(def.annotations))
40
+ return false;
41
+ return isToolVersionCompatible(bridge, def);
42
+ }
43
+ /** The visible tool names of a built-in group for the connected editor version + read-only state. */
44
+ function visibleToolNames(bridge, group, readOnly) {
45
+ return group.tools.filter((t) => {
46
+ const d = allDefs.get(t);
47
+ return d ? isToolVisible(bridge, d, readOnly) : false;
48
+ });
49
+ }
50
+ // ── Registration ─────────────────────────────────────────────────────
51
+ /**
52
+ * Register a single group's tools dynamically.
53
+ * Removes any existing stub for those tools first (stub->real swap).
54
+ * Returns the list of newly registered tool names.
55
+ */
56
+ export function registerGroupTools(server, bridge, group, readOnly) {
57
+ const registered = [];
58
+ for (const toolName of group.tools) {
59
+ const def = allDefs.get(toolName);
60
+ if (!def)
61
+ continue;
62
+ if (isExcludedByReadOnly(readOnly, def.annotations))
63
+ continue;
64
+ // Skip a tool the connected editor can't serve: registerToolWrapped would
65
+ // refuse it anyway, and an unconditional registered.push would make the
66
+ // activated summary advertise a tool that is absent from tools/list.
67
+ if (!isToolVersionCompatible(bridge, def))
68
+ continue;
69
+ removeToolByName(toolName); // Remove stub if present
70
+ registerToolWrapped(server, bridge, def.name, {
71
+ description: def.description,
72
+ inputSchema: def.inputSchema,
73
+ annotations: def.annotations,
74
+ }, createGroupToolHandler(bridge, def), { godotMinVersion: def.godotMinVersion, godotMaxVersion: def.godotMaxVersion });
75
+ registered.push(toolName);
76
+ }
77
+ return registered;
78
+ }
79
+ // ── discover_tools description builder ──────────────────────────────
80
+ /**
81
+ * Format one catalogue line for the discover_tools description, shared by the
82
+ * built-in and extension sections: `<name> [LOADED|available] — <desc>`. The
83
+ * loaded-state source and the description source differ per kind, so the caller
84
+ * resolves both and passes them in.
85
+ */
86
+ function formatGroupEntry(name, loaded, desc) {
87
+ return `${name} [${loaded ? "LOADED" : "available"}] — ${desc}`;
88
+ }
89
+ // Deliberately detailed description: as the gateway to 30+ hidden tools,
90
+ // discoverability matters more than brevity for this meta-tool.
91
+ //
92
+ // Format: group name + one-line description + status tag. No tool lists —
93
+ // agents see individual tools only after activation or via no-params catalog.
94
+ export function buildDiscoverToolsDesc(bridge, readOnly) {
95
+ const parts = [];
96
+ for (const group of GROUPS) {
97
+ // Drop a group with no tool the connected editor can serve — every member is
98
+ // version-gated out (or, in read-only mode, read-only-excluded). Keeps the
99
+ // meta description's group list aligned with what activation would register.
100
+ if (visibleToolNames(bridge, group, readOnly).length === 0)
101
+ continue;
102
+ const loaded = loadedGroups.has(group.name);
103
+ parts.push(formatGroupEntry(group.name, loaded, group.description));
104
+ }
105
+ const extParts = [];
106
+ for (const [name, ext] of extensionGroupEntries()) {
107
+ if (readOnly) {
108
+ const hasReadOnly = ext.commands.some((c) => isAllowedInReadOnly(c.annotations));
109
+ if (!hasReadOnly)
110
+ continue;
111
+ }
112
+ const loaded = isExtensionGroupLoaded(name);
113
+ const desc = ext.description || name;
114
+ extParts.push(formatGroupEntry(name, loaded, desc));
115
+ }
116
+ let description = "Find and activate tool groups by name or domain keyword. " +
117
+ "Activate only the groups needed for your current task (up to ~5) — loading many groups at once floods the tool list and degrades response quality. " +
118
+ "No params → full catalog. reset: true → deactivate ALL groups; reset: ['group_a'] → deactivate only group_a. " +
119
+ "Groups: " +
120
+ parts.join("; ");
121
+ if (extParts.length > 0) {
122
+ description += ". Extensions: " + extParts.join("; ");
123
+ }
124
+ description += ".";
125
+ return description;
126
+ }
127
+ // ── Group deactivation ──────────────────────────────────────────────
128
+ export function deactivateGroups(names, readOnly) {
129
+ const deactivated = [];
130
+ const targets = names === true ? [...loadedGroups, ...loadedExtensionGroupNames()] : names;
131
+ for (const groupName of targets) {
132
+ // Built-in group?
133
+ const group = GROUPS.find((g) => g.name === groupName);
134
+ if (group && loadedGroups.has(groupName)) {
135
+ for (const toolName of group.tools) {
136
+ const def = allDefs.get(toolName);
137
+ if (isExcludedByReadOnly(readOnly, def?.annotations))
138
+ continue;
139
+ removeToolByName(toolName);
140
+ }
141
+ loadedGroups.delete(groupName);
142
+ deactivated.push(groupName);
143
+ continue;
144
+ }
145
+ // Extension group?
146
+ if (deactivateExtensionGroup(groupName)) {
147
+ deactivated.push(groupName);
148
+ }
149
+ }
150
+ return deactivated;
151
+ }
152
+ // ── Activate / report (CQS split: command + query) ───────────
153
+ /**
154
+ * Activate a built-in group (the COMMAND side of the CQS split). Takes a
155
+ * resolved GroupDef and assumes activate intent; idempotent on an
156
+ * already-loaded group; in read-only mode an all-filtered group doesn't waste a
157
+ * slot. The caller dispatches built-in vs ext up front (activateGroupByName).
158
+ */
159
+ export function activateGroup(server, bridge, group, readOnly) {
160
+ // Tools visible for this editor version + read-only state (mirrors the
161
+ // registration gate) — drives the already-loaded summary below; the
162
+ // fresh-activate summary is the registerGroupTools return, same gate.
163
+ const toolNames = visibleToolNames(bridge, group, readOnly);
164
+ const tools = toolNames.map((t) => ({ name: t }));
165
+ if (loadedGroups.has(group.name)) {
166
+ return alreadyLoadedResult(group.name, tools, group.description);
167
+ }
168
+ const registered = registerGroupTools(server, bridge, group, readOnly);
169
+ // In read-only mode, if all tools were filtered out, don't waste a group slot.
170
+ if (readOnly && registered.length === 0) {
171
+ return readOnlyEmptyResult(group.name);
172
+ }
173
+ loadedGroups.add(group.name);
174
+ return activatedResult(group.name, registered, group.description);
175
+ }
176
+ /**
177
+ * Activate a group by name (the activate-side dispatcher): built-in name →
178
+ * activateGroup; otherwise delegate to the extension-group command
179
+ * (activateExtGroup).
180
+ */
181
+ export function activateGroupByName(server, bridge, name, readOnly) {
182
+ const group = GROUPS.find((g) => g.name === name);
183
+ if (!group)
184
+ return activateExtGroup(server, bridge, name, readOnly);
185
+ return activateGroup(server, bridge, group, readOnly);
186
+ }
187
+ /** Report a built-in group's status without mutating (the QUERY half). */
188
+ export function reportGroupStatus(bridge, groupName, readOnly) {
189
+ const group = GROUPS.find((g) => g.name === groupName);
190
+ if (!group)
191
+ return { name: groupName, status: "available", tools: [] };
192
+ // Tools visible for this editor version + read-only state (mirrors the registration gate).
193
+ const tools = visibleToolNames(bridge, group, readOnly).map((t) => ({ name: t }));
194
+ if (loadedGroups.has(groupName))
195
+ return alreadyLoadedResult(groupName, tools, group.description);
196
+ return availableResult(groupName, tools, group.description);
197
+ }
198
+ /**
199
+ * Report a group's status by name (the query-side dispatcher): built-in →
200
+ * reportGroupStatus, extension → reportExtGroupStatus, with readOnly passed
201
+ * through to the ext query. Routing ext browse here — rather than to the
202
+ * built-in reportGroupStatus, which returns empty for a non-built-in name —
203
+ * keeps the ext group's real tool list + already_loaded status.
204
+ */
205
+ export function reportGroupStatusByName(bridge, name, readOnly) {
206
+ const group = GROUPS.find((g) => g.name === name);
207
+ if (!group)
208
+ return reportExtGroupStatus(name, readOnly);
209
+ return reportGroupStatus(bridge, name, readOnly);
210
+ }
@@ -0,0 +1,52 @@
1
+ // Canonical tool inventory (single source of truth for counting + lookup).
2
+ // A leaf module that does NOT import groups.ts, so tool-def modules never
3
+ // cycle back here via catalogue.ts.
4
+ import { ALL_TOOL_DEFS } from "../registration/catalogue.js";
5
+ // Canonical group catalogue — the ordered GROUPS array, assembled from the 28
6
+ // per-group data modules (src/groups/defs/*) by builtinGroups.ts. Imported here
7
+ // as a local binding (NOT a bare re-export) so the eager derived sets below —
8
+ // GROUP_TOOL_NAMES — read it directly. ESM guarantees builtinGroups.ts and its
9
+ // 28 group imports fully execute before this module's body runs, so GROUPS is
10
+ // fully assembled when those derivations compute.
11
+ import { GROUPS } from "./builtinGroups.js";
12
+ // ── Group definitions ────────────────────────────────────────────────
13
+ // GROUP_NAMES — the canonical group order, DERIVED from GROUPS rather than
14
+ // hand-kept, so the value can never drift from the assembly order in
15
+ // builtinGroups.ts. ESM-safe: GROUPS is imported (fully assembled) above before
16
+ // this module's body runs, exactly like the eager GROUP_TOOL_NAMES derivation
17
+ // below. The GroupName[] element type is preserved (GroupDef.name is GroupName)
18
+ // — the central GroupName union in groupTypes.ts stays the type SSOT; only this
19
+ // value array is derived.
20
+ export const GROUP_NAMES = GROUPS.map((g) => g.name);
21
+ // GROUPS is assembled in builtinGroups.ts from the 28 per-group data modules
22
+ // (src/groups/defs/*), in canonical order. Imported above as a local binding and
23
+ // re-exported here so this module stays the single resolution point for GROUPS.
24
+ export { GROUPS };
25
+ /** All tool names that belong to groups (for filtering during eager tool registration). */
26
+ export const GROUP_TOOL_NAMES = new Set(GROUPS.flatMap((g) => g.tools));
27
+ // ── Tool lookup ──────────────────────────────────────────────────────
28
+ // Master lookup of every ToolDef by name, derived from the canonical
29
+ // ALL_TOOL_DEFS (catalogue.ts) so the lookup can never drift from the
30
+ // counted set. Eager const — no cycle, because catalogue.ts does not import
31
+ // groups.ts (group-loaded state lives in the leaf groupState.ts). This map
32
+ // is a superset of the group tools (it also holds eager-only tools like
33
+ // node/playtest); group code only ever looks up names it knows are group
34
+ // tools, so the extra entries are inert.
35
+ export const allDefs = new Map(ALL_TOOL_DEFS.map((t) => [t.name, t]));
36
+ // Tools that route through the runtime (Mode B) bridge rather than the editor
37
+ // bridge. runtime_set_property is an on-demand group tool (not eager) but still
38
+ // routes via the runtime bridge, so it belongs in this set. The catalogue
39
+ // completeness guard requires every runtime-bridge tool to resolve in
40
+ // ALL_TOOL_NAMES.
41
+ export const RUNTIME_TOOLS = new Set(["runtime_get_node_state", "runtime_set_property", "animation_player_control"]);
42
+ // LSP tools — use their own TCP client, not the bridge. The catalogue
43
+ // completeness guard requires each to resolve in ALL_TOOL_NAMES.
44
+ export const LSP_TOOLS = new Set([
45
+ "lsp_diagnostics",
46
+ "lsp_hover",
47
+ "lsp_completion",
48
+ "lsp_definition",
49
+ "lsp_symbols",
50
+ "lsp_references",
51
+ "lsp_project_diagnostics",
52
+ ]);