@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.
- package/ATTRIBUTIONS.md +141 -0
- package/LICENSE +28 -0
- package/README.md +386 -4
- package/dist/extensions/extensionChanges.js +131 -0
- package/dist/extensions/extensionCommand.js +25 -0
- package/dist/extensions/extensionDiscovery.js +141 -0
- package/dist/extensions/extensionRegistrar.js +82 -0
- package/dist/extensions/extensions.js +33 -0
- package/dist/groups/builtinGroups.js +60 -0
- package/dist/groups/defs/3dTools.js +17 -0
- package/dist/groups/defs/animationAuthoring.js +16 -0
- package/dist/groups/defs/assetOps.js +6 -0
- package/dist/groups/defs/audio.js +6 -0
- package/dist/groups/defs/classdb.js +6 -0
- package/dist/groups/defs/cleanup.js +6 -0
- package/dist/groups/defs/debugger.js +6 -0
- package/dist/groups/defs/editorAdvanced.js +16 -0
- package/dist/groups/defs/inputMap.js +6 -0
- package/dist/groups/defs/layerNaming.js +6 -0
- package/dist/groups/defs/lspCodeAnalysis.js +23 -0
- package/dist/groups/defs/lspCodeNavigation.js +15 -0
- package/dist/groups/defs/navigation.js +17 -0
- package/dist/groups/defs/particles.js +21 -0
- package/dist/groups/defs/pathEditing.js +22 -0
- package/dist/groups/defs/placeholders.js +28 -0
- package/dist/groups/defs/procedural.js +6 -0
- package/dist/groups/defs/resourceIo.js +6 -0
- package/dist/groups/defs/runtimeAdvanced.js +17 -0
- package/dist/groups/defs/sceneAdvanced.js +6 -0
- package/dist/groups/defs/sceneInheritance.js +6 -0
- package/dist/groups/defs/signals.js +6 -0
- package/dist/groups/defs/spriteframes.js +16 -0
- package/dist/groups/defs/theme.js +6 -0
- package/dist/groups/defs/tilemap.js +6 -0
- package/dist/groups/defs/tileset.js +22 -0
- package/dist/groups/defs/tilesetEdit.js +21 -0
- package/dist/groups/defs/userData.js +6 -0
- package/dist/groups/extensionGroups.js +182 -0
- package/dist/groups/groupActivation.js +210 -0
- package/dist/groups/groupCatalogue.js +52 -0
- package/dist/groups/groupMatch.js +182 -0
- package/dist/groups/groupResult.js +16 -0
- package/dist/groups/groupState.js +15 -0
- package/dist/groups/groupToolHandlers.js +103 -0
- package/dist/groups/groupTypes.js +10 -0
- package/dist/groups/groups.js +205 -0
- package/dist/index.js +144 -0
- package/dist/lsp/lspClient.js +509 -0
- package/dist/lsp/lspLabels.js +105 -0
- package/dist/lsp/lspProjectScan.js +156 -0
- package/dist/lsp/lspSession.js +139 -0
- package/dist/lsp/lspStatusReporter.js +76 -0
- package/dist/lsp/lspUri.js +68 -0
- package/dist/mcp/prompts.js +59 -0
- package/dist/mcp/resources.js +113 -0
- package/dist/mcp/roots.js +30 -0
- package/dist/registration/catalogue.js +123 -0
- package/dist/registration/extensionCollision.js +38 -0
- package/dist/registration/operations.js +67 -0
- package/dist/registration/screenshotResponse.js +71 -0
- package/dist/registration/toolDispatch.js +76 -0
- package/dist/registration/toolMeta.js +104 -0
- package/dist/registration/toolRefs.js +48 -0
- package/dist/registration/toolRegistry.js +211 -0
- package/dist/registry.js +291 -0
- package/dist/registryLiveness.js +113 -0
- package/dist/security/pathGuard.js +97 -0
- package/dist/security/profiles.js +104 -0
- package/dist/security/untrusted.js +23 -0
- package/dist/shared/errorContract.js +154 -0
- package/dist/shared/errors.js +21 -0
- package/dist/shared/pagination.js +135 -0
- package/dist/shared/schemaCoercion.js +173 -0
- package/dist/shared/stableJson.js +27 -0
- package/dist/shared/types.js +1 -0
- package/dist/shared/version.js +89 -0
- package/dist/startup/cliArgs.js +103 -0
- package/dist/startup/configReload.js +57 -0
- package/dist/startup/hooks.js +89 -0
- package/dist/startup/lifecycle.js +59 -0
- package/dist/startup/portConfig.js +127 -0
- package/dist/startup/reconcile.js +81 -0
- package/dist/startup/registrars.js +59 -0
- package/dist/startup/serverMode.js +19 -0
- package/dist/startup/startupEnv.js +142 -0
- package/dist/tools/animation.js +88 -0
- package/dist/tools/asset.js +58 -0
- package/dist/tools/assetWrite.js +16 -0
- package/dist/tools/audio.js +40 -0
- package/dist/tools/classdb.js +43 -0
- package/dist/tools/collision.js +23 -0
- package/dist/tools/debug.js +46 -0
- package/dist/tools/diff.js +18 -0
- package/dist/tools/editor.js +222 -0
- package/dist/tools/file.js +38 -0
- package/dist/tools/folder.js +27 -0
- package/dist/tools/inputMap.js +54 -0
- package/dist/tools/layerNames.js +29 -0
- package/dist/tools/lsp.js +524 -0
- package/dist/tools/navigation.js +24 -0
- package/dist/tools/node.js +145 -0
- package/dist/tools/nodeManagement.js +82 -0
- package/dist/tools/particles.js +83 -0
- package/dist/tools/path.js +30 -0
- package/dist/tools/playtest.js +99 -0
- package/dist/tools/procedural.js +101 -0
- package/dist/tools/resource.js +47 -0
- package/dist/tools/runtime.js +338 -0
- package/dist/tools/save.js +64 -0
- package/dist/tools/scene.js +102 -0
- package/dist/tools/sceneInheritance.js +19 -0
- package/dist/tools/sceneQuery.js +38 -0
- package/dist/tools/script.js +75 -0
- package/dist/tools/signals.js +53 -0
- package/dist/tools/sound.js +31 -0
- package/dist/tools/spatial.js +41 -0
- package/dist/tools/spriteframes.js +94 -0
- package/dist/tools/texture.js +35 -0
- package/dist/tools/theme.js +35 -0
- package/dist/tools/threeD.js +116 -0
- package/dist/tools/tilemap.js +51 -0
- package/dist/tools/tileset.js +226 -0
- package/dist/transport/authHandshake.js +43 -0
- package/dist/transport/bridge.js +225 -0
- package/dist/transport/channel.js +355 -0
- package/dist/transport/heartbeat.js +54 -0
- package/dist/transport/runtimeConnection.js +239 -0
- package/dist/transport/tokenPath.js +98 -0
- 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,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,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 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
|
+
]);
|