@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.
- 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 +82 -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 +22 -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 +352 -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 +97 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the read-only Godot resources (`godot://scene/{path}`,
|
|
3
|
+
* `godot://script/{path}`, `godot://project/info`) on the server. Each fetches
|
|
4
|
+
* live state over the bridge and degrades to an error payload if the call fails.
|
|
5
|
+
*/
|
|
6
|
+
export function registerResources(server, bridge) {
|
|
7
|
+
// ── godot://scene/{path} ─────────────────────────────────────────────
|
|
8
|
+
// Returns the scene tree snapshot for the given scene file path.
|
|
9
|
+
// The path should be a res:// path (e.g. "res://Main.tscn").
|
|
10
|
+
server.resource("scene", "godot://scene/{path}", { mimeType: "application/json" }, async (uri) => {
|
|
11
|
+
const path = decodeScenePath(uri.href);
|
|
12
|
+
try {
|
|
13
|
+
const result = await bridge.call("scene.get_tree", {
|
|
14
|
+
depth: 4,
|
|
15
|
+
include_properties: false,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
contents: [
|
|
19
|
+
{
|
|
20
|
+
uri: uri.href,
|
|
21
|
+
mimeType: "application/json",
|
|
22
|
+
text: JSON.stringify(result, null, 2),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return {
|
|
29
|
+
contents: [
|
|
30
|
+
{
|
|
31
|
+
uri: uri.href,
|
|
32
|
+
mimeType: "application/json",
|
|
33
|
+
text: JSON.stringify({
|
|
34
|
+
error: err.message,
|
|
35
|
+
path,
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// ── godot://script/{path} ───────────────────────────────────────────
|
|
43
|
+
// Returns the script source for a given res:// path.
|
|
44
|
+
server.resource("script", "godot://script/{path}", { mimeType: "text/x-gdscript" }, async (uri) => {
|
|
45
|
+
const path = decodeScriptPath(uri.href);
|
|
46
|
+
try {
|
|
47
|
+
const result = (await bridge.call("script.read", {
|
|
48
|
+
file_path: path,
|
|
49
|
+
}));
|
|
50
|
+
return {
|
|
51
|
+
contents: [
|
|
52
|
+
{
|
|
53
|
+
uri: uri.href,
|
|
54
|
+
mimeType: "text/x-gdscript",
|
|
55
|
+
text: typeof result?.content === "string" ? result.content : JSON.stringify(result),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return {
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
uri: uri.href,
|
|
65
|
+
mimeType: "text/plain",
|
|
66
|
+
text: `Error reading script: ${err.message}`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// ── godot://project/info ─────────────────────────────────────────────
|
|
73
|
+
// Returns project metadata (name, Godot version, etc.).
|
|
74
|
+
server.resource("project-info", "godot://project/info", { mimeType: "application/json" }, async (uri) => {
|
|
75
|
+
try {
|
|
76
|
+
const result = await bridge.call("project.get_settings", {
|
|
77
|
+
keys: ["application/config/name", "application/config/version"],
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
contents: [
|
|
81
|
+
{
|
|
82
|
+
uri: uri.href,
|
|
83
|
+
mimeType: "application/json",
|
|
84
|
+
text: JSON.stringify(result, null, 2),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return {
|
|
91
|
+
contents: [
|
|
92
|
+
{
|
|
93
|
+
uri: uri.href,
|
|
94
|
+
mimeType: "application/json",
|
|
95
|
+
text: JSON.stringify({ error: err.message }),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// ── URI helpers ───────────────────────────────────────────────────────
|
|
103
|
+
/** Extract the res:// path from a godot://scene/ URI. */
|
|
104
|
+
function decodeScenePath(href) {
|
|
105
|
+
// godot://scene/res://Main.tscn → res://Main.tscn
|
|
106
|
+
const match = href.match(/^godot:\/\/scene\/(.+)$/);
|
|
107
|
+
return match ? decodeURIComponent(match[1]) : href;
|
|
108
|
+
}
|
|
109
|
+
/** Extract the res:// path from a godot://script/ URI. */
|
|
110
|
+
function decodeScriptPath(href) {
|
|
111
|
+
const match = href.match(/^godot:\/\/script\/(.+)$/);
|
|
112
|
+
return match ? decodeURIComponent(match[1]) : href;
|
|
113
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Resolved project path — set once at startup via init(). */
|
|
2
|
+
let projectRoot;
|
|
3
|
+
/** Set the resolved project root (startup wiring). @internal */
|
|
4
|
+
export function init(path) {
|
|
5
|
+
projectRoot = path;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Register a `godot://roots` resource that returns the project root(s).
|
|
9
|
+
* This lets MCP clients discover what Godot project this server is
|
|
10
|
+
* connected to without relying on the client's own root list.
|
|
11
|
+
*/
|
|
12
|
+
export function registerRoots(server) {
|
|
13
|
+
server.resource("roots", "godot://roots", { mimeType: "application/json" }, async (uri) => {
|
|
14
|
+
const roots = [];
|
|
15
|
+
if (projectRoot) {
|
|
16
|
+
// Normalize to file:// URI for cross-platform compatibility.
|
|
17
|
+
const fileUri = projectRoot.startsWith("file://") ? projectRoot : `file://${projectRoot.replace(/\\/g, "/")}`;
|
|
18
|
+
roots.push({ uri: fileUri, name: "Godot Project" });
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
contents: [
|
|
22
|
+
{
|
|
23
|
+
uri: uri.href,
|
|
24
|
+
mimeType: "application/json",
|
|
25
|
+
text: JSON.stringify({ roots }, null, 2),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// ── Canonical tool catalogue ─────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// THE single source of truth for "every tool definition the server ships".
|
|
4
|
+
// ALL_TOOL_DEFS is the complete, deduplicated list of every per-module
|
|
5
|
+
// ToolDef array under src/tools/. Counting (the --tools-count CLI flag),
|
|
6
|
+
// the structural smoke catalogue (test/sections/01_catalogue.ts), the
|
|
7
|
+
// unfiltered structural checks (test/structural.ts), and the groups.ts
|
|
8
|
+
// name→def lookup all derive from this list — so a tool can never be
|
|
9
|
+
// counted in one place and missed in another.
|
|
10
|
+
//
|
|
11
|
+
// GUARDRAIL — enumeration only. This list is for counting and static
|
|
12
|
+
// validation. It is NOT the registration path: the eager set is still
|
|
13
|
+
// `allowed − GROUP_TOOL_NAMES` fed to per-module registerTools() in
|
|
14
|
+
// index.ts. Do not route runtime registration through ALL_TOOL_DEFS — it
|
|
15
|
+
// would eagerly advertise every tool and defeat the on-demand split.
|
|
16
|
+
//
|
|
17
|
+
// Maintenance: when a new src/tools/ module is added, import its ToolDef
|
|
18
|
+
// array here. The completeness guard in 01_catalogue.ts (GROUP/RUNTIME/LSP
|
|
19
|
+
// tool names ⊆ ALL_TOOL_NAMES) plus the no-duplicate-names assertion catch
|
|
20
|
+
// the common mistakes (forgotten array, double-counted convenience export).
|
|
21
|
+
import { animationTools } from "../tools/animation.js";
|
|
22
|
+
import { assetTools } from "../tools/asset.js";
|
|
23
|
+
import { audioTools } from "../tools/audio.js";
|
|
24
|
+
import { classdbTools } from "../tools/classdb.js";
|
|
25
|
+
import { collisionTools } from "../tools/collision.js";
|
|
26
|
+
import { debugTools } from "../tools/debug.js";
|
|
27
|
+
import { diffTools } from "../tools/diff.js";
|
|
28
|
+
import { editorTools } from "../tools/editor.js";
|
|
29
|
+
import { fileTools } from "../tools/file.js";
|
|
30
|
+
import { folderTools } from "../tools/folder.js";
|
|
31
|
+
import { inputMapTools } from "../tools/inputMap.js";
|
|
32
|
+
import { layerNameTools } from "../tools/layerNames.js";
|
|
33
|
+
import { lspAnalysisTools, lspNavigationTools } from "../tools/lsp.js";
|
|
34
|
+
import { navigationTools } from "../tools/navigation.js";
|
|
35
|
+
import { nodeTools } from "../tools/node.js";
|
|
36
|
+
import { nodeManagementTools } from "../tools/nodeManagement.js";
|
|
37
|
+
import { particleTools } from "../tools/particles.js";
|
|
38
|
+
import { pathTools } from "../tools/path.js";
|
|
39
|
+
import { playtestTools } from "../tools/playtest.js";
|
|
40
|
+
import { proceduralTools } from "../tools/procedural.js";
|
|
41
|
+
import { resourceTools } from "../tools/resource.js";
|
|
42
|
+
import { runtimeTools } from "../tools/runtime.js";
|
|
43
|
+
import { saveTools } from "../tools/save.js";
|
|
44
|
+
import { sceneTools } from "../tools/scene.js";
|
|
45
|
+
import { sceneInheritanceTools } from "../tools/sceneInheritance.js";
|
|
46
|
+
import { sceneQueryTools } from "../tools/sceneQuery.js";
|
|
47
|
+
import { scriptTools } from "../tools/script.js";
|
|
48
|
+
import { signalTools } from "../tools/signals.js";
|
|
49
|
+
import { soundTools } from "../tools/sound.js";
|
|
50
|
+
import { spatialTools } from "../tools/spatial.js";
|
|
51
|
+
import { spriteframesTools } from "../tools/spriteframes.js";
|
|
52
|
+
import { themeTools } from "../tools/theme.js";
|
|
53
|
+
import { textureTools } from "../tools/texture.js";
|
|
54
|
+
import { threeDTools } from "../tools/threeD.js";
|
|
55
|
+
import { tilemapTools } from "../tools/tilemap.js";
|
|
56
|
+
// NOTE: split tileset exports — NOT the unsplit `tilesetTools` convenience
|
|
57
|
+
// spread, which would double-count. Same reasoning applies to lsp above
|
|
58
|
+
// (lspAnalysisTools + lspNavigationTools, not the combined `lspTools`).
|
|
59
|
+
import { tilesetStructuralTools, tilesetEditTools } from "../tools/tileset.js";
|
|
60
|
+
/**
|
|
61
|
+
* Every tool definition the server ships, across all src/tools/ modules.
|
|
62
|
+
* Both eager and on-demand (group) tools live here — the eager/on-demand
|
|
63
|
+
* split is a visibility partition over this set (see serverMode.ts
|
|
64
|
+
* MODULE_ALLOWED and groups.ts GROUP_TOOL_NAMES), not two pools.
|
|
65
|
+
*/
|
|
66
|
+
export const ALL_TOOL_DEFS = [
|
|
67
|
+
...animationTools,
|
|
68
|
+
...assetTools,
|
|
69
|
+
...audioTools,
|
|
70
|
+
...classdbTools,
|
|
71
|
+
...collisionTools,
|
|
72
|
+
...debugTools,
|
|
73
|
+
...diffTools,
|
|
74
|
+
...editorTools,
|
|
75
|
+
...fileTools,
|
|
76
|
+
...folderTools,
|
|
77
|
+
...inputMapTools,
|
|
78
|
+
...layerNameTools,
|
|
79
|
+
...lspAnalysisTools,
|
|
80
|
+
...lspNavigationTools,
|
|
81
|
+
...navigationTools,
|
|
82
|
+
...nodeTools,
|
|
83
|
+
...nodeManagementTools,
|
|
84
|
+
...particleTools,
|
|
85
|
+
...pathTools,
|
|
86
|
+
...playtestTools,
|
|
87
|
+
...proceduralTools,
|
|
88
|
+
...resourceTools,
|
|
89
|
+
...runtimeTools,
|
|
90
|
+
...saveTools,
|
|
91
|
+
...sceneTools,
|
|
92
|
+
...sceneInheritanceTools,
|
|
93
|
+
...sceneQueryTools,
|
|
94
|
+
...scriptTools,
|
|
95
|
+
...signalTools,
|
|
96
|
+
...soundTools,
|
|
97
|
+
...spatialTools,
|
|
98
|
+
...spriteframesTools,
|
|
99
|
+
...themeTools,
|
|
100
|
+
...threeDTools,
|
|
101
|
+
...textureTools,
|
|
102
|
+
...tilemapTools,
|
|
103
|
+
...tilesetStructuralTools,
|
|
104
|
+
...tilesetEditTools,
|
|
105
|
+
];
|
|
106
|
+
/** Names of every tool in ALL_TOOL_DEFS. */
|
|
107
|
+
export const ALL_TOOL_NAMES = new Set(ALL_TOOL_DEFS.map((t) => t.name));
|
|
108
|
+
/**
|
|
109
|
+
* Always-registered tools that live OUTSIDE the per-module ToolDef arrays
|
|
110
|
+
* (registered directly in index.ts / groups.ts, so absent from
|
|
111
|
+
* ALL_TOOL_DEFS). Keep in sync with index.ts registerGroups() +
|
|
112
|
+
* registerExtensionsRefresh(). Excludes per-project extension tools, which
|
|
113
|
+
* are dynamic.
|
|
114
|
+
*/
|
|
115
|
+
export const META_TOOL_NAMES = ["discover_tools", "extensions_refresh"];
|
|
116
|
+
/**
|
|
117
|
+
* Whether `name` is one of the server's own built-in tool names — any tool in the
|
|
118
|
+
* static catalogue (eager or on-demand) or an always-on meta tool. The authority
|
|
119
|
+
* for "this name belongs to the server, not to an extension."
|
|
120
|
+
*/
|
|
121
|
+
export function isBuiltinToolName(name) {
|
|
122
|
+
return ALL_TOOL_NAMES.has(name) || META_TOOL_NAMES.includes(name);
|
|
123
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension name-collision guard.
|
|
3
|
+
*
|
|
4
|
+
* Refuses an extension tool whose name would shadow a built-in tool or a tool
|
|
5
|
+
* that is already registered. The server is the only component that holds BOTH
|
|
6
|
+
* its full built-in catalogue and a discovered extension's chosen name at
|
|
7
|
+
* registration time — the toolkit publishes the extensions but never sees the
|
|
8
|
+
* server's built-in list — so a built-in↔extension name clash can only be caught
|
|
9
|
+
* here.
|
|
10
|
+
*
|
|
11
|
+
* Installed extensions run in-process with full trust, so refusing a clash is
|
|
12
|
+
* defence-in-depth against ACCIDENTAL name reuse (a well-meaning author reuses a
|
|
13
|
+
* name the server already ships), not a privilege boundary: the clash is skipped
|
|
14
|
+
* with a loud warning, never a crash, and the incumbent always wins — a built-in
|
|
15
|
+
* is never overwritten, and between two extensions the first to register keeps the
|
|
16
|
+
* name.
|
|
17
|
+
*/
|
|
18
|
+
import { isBuiltinToolName } from "./catalogue.js";
|
|
19
|
+
import { hasToolRef } from "./toolRefs.js";
|
|
20
|
+
/**
|
|
21
|
+
* Whether registering an extension tool under `toolName` would collide with a
|
|
22
|
+
* built-in tool or an already-registered tool; the caller MUST skip the tool when
|
|
23
|
+
* this returns true (the incumbent keeps the name). A collision is logged to
|
|
24
|
+
* stderr so the skip is diagnosable; a free name returns false silently.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* The explicit pre-check makes the MCP SDK's duplicate-name handling moot for
|
|
28
|
+
* correctness: the SDK's `registerTool` throws `Tool <name> is already registered`
|
|
29
|
+
* on a repeated name, so refusing the collision here skips the one tool cleanly
|
|
30
|
+
* instead of letting that throw abort the surrounding registration batch.
|
|
31
|
+
*/
|
|
32
|
+
export function extensionNameCollides(toolName) {
|
|
33
|
+
if (isBuiltinToolName(toolName) || hasToolRef(toolName)) {
|
|
34
|
+
process.stderr.write(`[godot-mcp] extension tool '${toolName}' collides with a built-in (or already-registered) tool — skipped\n`);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation-coverage counting — the single derivation of how many distinct
|
|
3
|
+
* *operations* (action-discriminator values) the built-in tool surface exposes.
|
|
4
|
+
*
|
|
5
|
+
* An action-consolidated tool packs several operations behind one enum
|
|
6
|
+
* discriminator (`node_manage.action` → rename/reparent/reorder/duplicate), so a
|
|
7
|
+
* raw tool count understates the real breadth. This module is the SSOT the
|
|
8
|
+
* `--tools-count` CLI, the `01_catalogue` drift gate, and the tool-reference
|
|
9
|
+
* generator all count through, so the number can never differ between them.
|
|
10
|
+
*
|
|
11
|
+
* Human-facing only: operation counts feed reports and docs, never a tool
|
|
12
|
+
* description or `discover_tools` output.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
/**
|
|
18
|
+
* The distinct operations a tool exposes through its discriminator: the values
|
|
19
|
+
* of its `operationParam` enum, else its explicit `operations` list, else an
|
|
20
|
+
* empty array for a tool that performs one implicit operation with no
|
|
21
|
+
* discriminator.
|
|
22
|
+
*
|
|
23
|
+
* @param def one catalogue tool definition
|
|
24
|
+
* @returns the operation value names, or an empty array for a single-operation
|
|
25
|
+
* tool — callers that need a count floor it at 1 (see {@link operationCountOf})
|
|
26
|
+
* @remarks Reads the enum values via `z.toJSONSchema` (the codebase idiom, robust
|
|
27
|
+
* across Zod point releases) rather than the Zod-instance enum API. A missing or
|
|
28
|
+
* non-enum `operationParam` yields no values; the drift gate is what asserts the
|
|
29
|
+
* param names a real enum.
|
|
30
|
+
*/
|
|
31
|
+
export function operationsOf(def) {
|
|
32
|
+
if (def.operationParam) {
|
|
33
|
+
const values = enumValuesOf(def.inputSchema, def.operationParam);
|
|
34
|
+
if (values.length > 0)
|
|
35
|
+
return values;
|
|
36
|
+
}
|
|
37
|
+
if (def.operations && def.operations.length > 0)
|
|
38
|
+
return def.operations;
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
/** A tool's operation count — its discriminator values, or 1 for a single-operation tool. */
|
|
42
|
+
export function operationCountOf(def) {
|
|
43
|
+
return Math.max(1, operationsOf(def).length);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Total built-in operations across every catalogued tool — the sum of each
|
|
47
|
+
* tool's {@link operationCountOf}.
|
|
48
|
+
*
|
|
49
|
+
* @param defs the canonical catalogue (`ALL_TOOL_DEFS`)
|
|
50
|
+
* @returns the operation grand total (the figure `--tools-count` prints and the
|
|
51
|
+
* drift gate snapshots)
|
|
52
|
+
*/
|
|
53
|
+
export function countBuiltinOperations(defs) {
|
|
54
|
+
let total = 0;
|
|
55
|
+
for (const def of defs)
|
|
56
|
+
total += operationCountOf(def);
|
|
57
|
+
return total;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The enum values of a top-level inputSchema param, or an empty array when the
|
|
61
|
+
* param is absent or not a plain enum (e.g. a string, or an enum nested in a
|
|
62
|
+
* union — those tools declare `operations` instead).
|
|
63
|
+
*/
|
|
64
|
+
function enumValuesOf(inputSchema, param) {
|
|
65
|
+
const json = z.toJSONSchema(z.object(inputSchema));
|
|
66
|
+
return json.properties?.[param]?.enum ?? [];
|
|
67
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared response builder for screenshot tools — the one place the image-first
|
|
3
|
+
* multi-content shape lives, shared by the editor and runtime screenshot tools.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Build a screenshot tool's response from the toolkit payload.
|
|
7
|
+
*
|
|
8
|
+
* When `imageBase64` is present the response is image-first: the captured image
|
|
9
|
+
* block, then a JSON metadata text block — the agent sees the picture, then the
|
|
10
|
+
* dimensions. When it is absent (a `disk`-mode capture that persisted the PNG
|
|
11
|
+
* and returned only its path), the response is a single lean text block naming
|
|
12
|
+
* the on-disk `path` — no empty image block.
|
|
13
|
+
*
|
|
14
|
+
* @param imageBase64 base64 image bytes from the toolkit (`image_base64`), or
|
|
15
|
+
* `undefined` for a disk-only capture that persisted the PNG.
|
|
16
|
+
* @param mimeType image MIME type; the image block falls back to "image/png"
|
|
17
|
+
* when absent, and the disk text block omits it when absent.
|
|
18
|
+
* @param meta metadata serialized into the text block. `path` is the
|
|
19
|
+
* saved file path (disk/both) or a node echo (inline node
|
|
20
|
+
* focus); `hint` is any toolkit guidance to relay; `remediation`
|
|
21
|
+
* names a visible side effect the toolkit took (main-screen
|
|
22
|
+
* switch, foregrounding); `image_detail` is the detail level the
|
|
23
|
+
* toolkit applied to the inline image (`full`/`mid`/`low`);
|
|
24
|
+
* `returned` is the returned image's `"WxH"` — for disk-only the
|
|
25
|
+
* full-res dims of the saved file. Both are relayed verbatim from
|
|
26
|
+
* the toolkit payload — this builder never recomputes dimensions.
|
|
27
|
+
* Undefined keys are dropped by `JSON.stringify`, so each appears
|
|
28
|
+
* only when present.
|
|
29
|
+
*/
|
|
30
|
+
export function buildScreenshotResult(imageBase64, mimeType, meta) {
|
|
31
|
+
if (imageBase64 === undefined) {
|
|
32
|
+
// Disk-only capture: the toolkit saved the PNG and returned just its path.
|
|
33
|
+
// Lean text envelope (path first — the actionable field), no image block.
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: JSON.stringify({
|
|
39
|
+
path: meta.path,
|
|
40
|
+
width: meta.width,
|
|
41
|
+
height: meta.height,
|
|
42
|
+
bytes: meta.bytes,
|
|
43
|
+
mime_type: mimeType,
|
|
44
|
+
remediation: meta.remediation,
|
|
45
|
+
hint: meta.hint,
|
|
46
|
+
image_detail: meta.image_detail,
|
|
47
|
+
returned: meta.returned,
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{ type: "image", data: imageBase64, mimeType: mimeType ?? "image/png" },
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: JSON.stringify({
|
|
59
|
+
width: meta.width,
|
|
60
|
+
height: meta.height,
|
|
61
|
+
bytes: meta.bytes,
|
|
62
|
+
path: meta.path,
|
|
63
|
+
remediation: meta.remediation,
|
|
64
|
+
hint: meta.hint,
|
|
65
|
+
image_detail: meta.image_detail,
|
|
66
|
+
returned: meta.returned,
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool dispatch — execute ONE tool call: route it to the bridge, normalize
|
|
3
|
+
* the result into the canonical MCP error contract, and inject the
|
|
4
|
+
* happy-path success hint. The per-call primitive that registration wires
|
|
5
|
+
* every default tool's handler through; it knows nothing of registration
|
|
6
|
+
* (no version gate, path guard, or hook pipeline — those wrap the handler
|
|
7
|
+
* one layer above, in the registration core's wrappedHandler). A mid-tier module:
|
|
8
|
+
* below registration, above the error/serialization leaves.
|
|
9
|
+
*/
|
|
10
|
+
import { stableStringify } from "../shared/stableJson.js";
|
|
11
|
+
import { BridgeError } from "../shared/errors.js";
|
|
12
|
+
import { toolError, toolErrorFromPayload, toolErrorFromException, runtimeErrorWithCrashContext, } from "../shared/errorContract.js";
|
|
13
|
+
/**
|
|
14
|
+
* Whether a success hint should be applied: the payload is a non-null
|
|
15
|
+
* object with no existing hint. The server never overwrites a
|
|
16
|
+
* toolkit-provided hint. Shared by both injection sites — the raw-object
|
|
17
|
+
* path in callAndWrap and the parsed-text-block path in injectSuccessHint —
|
|
18
|
+
* which keep their distinct serialization
|
|
19
|
+
* steps; only this boolean decision is unified.
|
|
20
|
+
*/
|
|
21
|
+
function shouldApplySuccessHint(payload) {
|
|
22
|
+
return !!payload && typeof payload === "object" && !payload.hint;
|
|
23
|
+
}
|
|
24
|
+
// ── Shared call wrapper ─────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Shared handler body for tools that do a single bridge call and
|
|
27
|
+
* JSON-stringify the result. Centralises error-contract compliance:
|
|
28
|
+
* 1. Try/catch around the bridge call — BridgeError becomes toolError.
|
|
29
|
+
* 2. Result payload inspection — {success: false} becomes toolError.
|
|
30
|
+
* 3. Happy path — JSON-stringified into a text content block.
|
|
31
|
+
*
|
|
32
|
+
* Screenshots and other multi-content handlers stay custom but use
|
|
33
|
+
* toolError directly for their error branches.
|
|
34
|
+
*/
|
|
35
|
+
export async function callAndWrap(bridge, method, input, opts = {}) {
|
|
36
|
+
try {
|
|
37
|
+
const result = opts.runtime
|
|
38
|
+
? await bridge.callRuntime(method, input, opts.timeoutMs, opts.signal)
|
|
39
|
+
: await bridge.call(method, input, opts.timeoutMs, opts.signal);
|
|
40
|
+
const err = toolErrorFromPayload(result);
|
|
41
|
+
if (err)
|
|
42
|
+
return err;
|
|
43
|
+
// Inject success hint if provided and toolkit didn't already set one
|
|
44
|
+
if (opts.successHint && shouldApplySuccessHint(result))
|
|
45
|
+
result.hint = opts.successHint;
|
|
46
|
+
return { content: [{ type: "text", text: stableStringify(result) }] };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (opts.runtime)
|
|
50
|
+
return runtimeErrorWithCrashContext(bridge, err);
|
|
51
|
+
if (opts.extensionTimeoutHint && err instanceof BridgeError && err.code === "TIMEOUT") {
|
|
52
|
+
return toolError("TIMEOUT", err.message, opts.extensionTimeoutHint);
|
|
53
|
+
}
|
|
54
|
+
return toolErrorFromException(err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// ── Success-hint injection (custom-handler path) ────────────────────
|
|
58
|
+
/** Inject a success hint into the first JSON text block of a ToolTextResult.
|
|
59
|
+
* Skips if the payload already has a toolkit-provided hint. */
|
|
60
|
+
export function injectSuccessHint(result, hint) {
|
|
61
|
+
for (const block of result.content) {
|
|
62
|
+
if (block.type === "text") {
|
|
63
|
+
try {
|
|
64
|
+
const payload = JSON.parse(block.text);
|
|
65
|
+
if (shouldApplySuccessHint(payload)) {
|
|
66
|
+
payload.hint = hint;
|
|
67
|
+
block.text = JSON.stringify(payload);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* non-JSON text content — skip */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool metadata enrichment for discover_tools responses.
|
|
3
|
+
*
|
|
4
|
+
* Converts ToolDef (Zod schemas) and ExtensionCmd (raw JSON Schema) into
|
|
5
|
+
* lightweight, LLM-readable metadata objects. Used by the discover_tools
|
|
6
|
+
* handler to enrich activation responses so agents can call tools without
|
|
7
|
+
* a separate schema lookup round-trip.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
// ── JSON Schema → param map (reverse of jsonSchemaToZodShape) ──────
|
|
11
|
+
/**
|
|
12
|
+
* Flatten a JSON Schema properties/required structure to a simplified
|
|
13
|
+
* parameter map. Mirrors jsonSchemaToZodShape() in reverse to build the
|
|
14
|
+
* human-readable param info for discover_tools enrichment. Handles the same
|
|
15
|
+
* types as jsonSchemaToZodShape.
|
|
16
|
+
*/
|
|
17
|
+
export function jsonSchemaToParamMap(schema) {
|
|
18
|
+
const properties = schema.properties;
|
|
19
|
+
if (!properties)
|
|
20
|
+
return {};
|
|
21
|
+
const required = new Set(schema.required ?? []);
|
|
22
|
+
const params = {};
|
|
23
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
24
|
+
let type;
|
|
25
|
+
switch (prop.type) {
|
|
26
|
+
case "string":
|
|
27
|
+
type = Array.isArray(prop.enum) && prop.enum.length > 0 ? "enum" : "string";
|
|
28
|
+
break;
|
|
29
|
+
case "number":
|
|
30
|
+
case "integer":
|
|
31
|
+
type = "number";
|
|
32
|
+
break;
|
|
33
|
+
case "boolean":
|
|
34
|
+
type = "boolean";
|
|
35
|
+
break;
|
|
36
|
+
case "array":
|
|
37
|
+
type = "array";
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
type = "string";
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const description = typeof prop.description === "string" ? prop.description : undefined;
|
|
44
|
+
params[key] = { type, required: required.has(key), ...(description && { description }) };
|
|
45
|
+
}
|
|
46
|
+
return params;
|
|
47
|
+
}
|
|
48
|
+
// ── Enrichment helpers ───────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Build a ToolMeta from a built-in ToolDef.
|
|
51
|
+
* Converts the Zod inputSchema to JSON Schema via z.toJSONSchema(),
|
|
52
|
+
* then flattens to a simplified param map.
|
|
53
|
+
*/
|
|
54
|
+
function enrichBuiltinTool(def, includeSchemas) {
|
|
55
|
+
const meta = { name: def.name, description: def.description };
|
|
56
|
+
if (includeSchemas) {
|
|
57
|
+
const jsonSchema = z.toJSONSchema(z.object(def.inputSchema));
|
|
58
|
+
meta.parameters = jsonSchemaToParamMap(jsonSchema);
|
|
59
|
+
if (def.annotations)
|
|
60
|
+
meta.annotations = def.annotations;
|
|
61
|
+
}
|
|
62
|
+
return meta;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build a ToolMeta from an ExtensionCmd.
|
|
66
|
+
* Extension schemas are already raw JSON Schema — flatten directly.
|
|
67
|
+
*/
|
|
68
|
+
function enrichExtensionTool(cmd, includeSchemas) {
|
|
69
|
+
const meta = { name: cmd.toolName, description: cmd.description };
|
|
70
|
+
if (includeSchemas) {
|
|
71
|
+
meta.parameters = jsonSchemaToParamMap(cmd.inputSchema);
|
|
72
|
+
if (cmd.annotations)
|
|
73
|
+
meta.annotations = cmd.annotations;
|
|
74
|
+
}
|
|
75
|
+
return meta;
|
|
76
|
+
}
|
|
77
|
+
// ── Post-collection enrichment ───────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Enrich group results after collection. For activated/already_loaded
|
|
80
|
+
* groups, replace bare tool names with full metadata. For available
|
|
81
|
+
* groups, tools stay as {name} only.
|
|
82
|
+
*
|
|
83
|
+
* @param results - Raw group results from the discover_tools handler (activateGroupByName / reportGroupStatusByName)
|
|
84
|
+
* @param includeSchemas - Whether to include parameters + annotations
|
|
85
|
+
* @param allDefs - Master lookup of all built-in ToolDefs by name
|
|
86
|
+
* @param extGroupCommands - Lookup of extension commands by tool name
|
|
87
|
+
*/
|
|
88
|
+
export function enrichGroupResults(results, includeSchemas, allDefs, extGroupCommands) {
|
|
89
|
+
for (const result of results) {
|
|
90
|
+
if (result.status !== "activated" && result.status !== "already_loaded")
|
|
91
|
+
continue;
|
|
92
|
+
result.tools = result.tools.map((tool) => {
|
|
93
|
+
const def = allDefs.get(tool.name);
|
|
94
|
+
if (def)
|
|
95
|
+
return enrichBuiltinTool(def, includeSchemas);
|
|
96
|
+
const extCmd = extGroupCommands.get(tool.name);
|
|
97
|
+
if (extCmd)
|
|
98
|
+
return enrichExtensionTool(extCmd, includeSchemas);
|
|
99
|
+
// Fallback: tool name only (shouldn't happen for loaded groups).
|
|
100
|
+
return tool;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool-ref registry. Tracks RegisteredTool refs returned by
|
|
3
|
+
* server.registerTool() so that tools can be surgically removed by
|
|
4
|
+
* name (stub->real swap in discover_tools) or bulk-removed
|
|
5
|
+
* (config reload).
|
|
6
|
+
*/
|
|
7
|
+
const toolRefs = new Map();
|
|
8
|
+
export function setToolRef(name, ref) {
|
|
9
|
+
toolRefs.set(name, ref);
|
|
10
|
+
}
|
|
11
|
+
/** Update a registered tool's properties in-place (one notification). */
|
|
12
|
+
export function updateToolRef(name, updates) {
|
|
13
|
+
const ref = toolRefs.get(name);
|
|
14
|
+
if (!ref?.update)
|
|
15
|
+
return false;
|
|
16
|
+
ref.update(updates);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
export function removeToolByName(name) {
|
|
20
|
+
const ref = toolRefs.get(name);
|
|
21
|
+
if (!ref)
|
|
22
|
+
return false;
|
|
23
|
+
try {
|
|
24
|
+
ref.remove();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* already removed */
|
|
28
|
+
}
|
|
29
|
+
toolRefs.delete(name);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export function removeAllToolRefs() {
|
|
33
|
+
for (const [, ref] of toolRefs) {
|
|
34
|
+
try {
|
|
35
|
+
ref.remove();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* already removed */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
toolRefs.clear();
|
|
42
|
+
}
|
|
43
|
+
export function hasToolRef(name) {
|
|
44
|
+
return toolRefs.has(name);
|
|
45
|
+
}
|
|
46
|
+
export function toolRefCount() {
|
|
47
|
+
return toolRefs.size;
|
|
48
|
+
}
|