@npgamedev/godot-mcp-server 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/ATTRIBUTIONS.md +141 -0
  2. package/LICENSE +28 -0
  3. package/README.md +386 -4
  4. package/dist/extensions/extensionChanges.js +131 -0
  5. package/dist/extensions/extensionCommand.js +25 -0
  6. package/dist/extensions/extensionDiscovery.js +141 -0
  7. package/dist/extensions/extensionRegistrar.js +82 -0
  8. package/dist/extensions/extensions.js +33 -0
  9. package/dist/groups/builtinGroups.js +60 -0
  10. package/dist/groups/defs/3dTools.js +17 -0
  11. package/dist/groups/defs/animationAuthoring.js +16 -0
  12. package/dist/groups/defs/assetOps.js +6 -0
  13. package/dist/groups/defs/audio.js +6 -0
  14. package/dist/groups/defs/classdb.js +6 -0
  15. package/dist/groups/defs/cleanup.js +6 -0
  16. package/dist/groups/defs/debugger.js +6 -0
  17. package/dist/groups/defs/editorAdvanced.js +16 -0
  18. package/dist/groups/defs/inputMap.js +6 -0
  19. package/dist/groups/defs/layerNaming.js +6 -0
  20. package/dist/groups/defs/lspCodeAnalysis.js +23 -0
  21. package/dist/groups/defs/lspCodeNavigation.js +15 -0
  22. package/dist/groups/defs/navigation.js +17 -0
  23. package/dist/groups/defs/particles.js +21 -0
  24. package/dist/groups/defs/pathEditing.js +22 -0
  25. package/dist/groups/defs/placeholders.js +28 -0
  26. package/dist/groups/defs/procedural.js +6 -0
  27. package/dist/groups/defs/resourceIo.js +6 -0
  28. package/dist/groups/defs/runtimeAdvanced.js +17 -0
  29. package/dist/groups/defs/sceneAdvanced.js +6 -0
  30. package/dist/groups/defs/sceneInheritance.js +6 -0
  31. package/dist/groups/defs/signals.js +6 -0
  32. package/dist/groups/defs/spriteframes.js +16 -0
  33. package/dist/groups/defs/theme.js +6 -0
  34. package/dist/groups/defs/tilemap.js +6 -0
  35. package/dist/groups/defs/tileset.js +22 -0
  36. package/dist/groups/defs/tilesetEdit.js +21 -0
  37. package/dist/groups/defs/userData.js +6 -0
  38. package/dist/groups/extensionGroups.js +182 -0
  39. package/dist/groups/groupActivation.js +210 -0
  40. package/dist/groups/groupCatalogue.js +52 -0
  41. package/dist/groups/groupMatch.js +182 -0
  42. package/dist/groups/groupResult.js +16 -0
  43. package/dist/groups/groupState.js +15 -0
  44. package/dist/groups/groupToolHandlers.js +103 -0
  45. package/dist/groups/groupTypes.js +10 -0
  46. package/dist/groups/groups.js +205 -0
  47. package/dist/index.js +144 -0
  48. package/dist/lsp/lspClient.js +509 -0
  49. package/dist/lsp/lspLabels.js +105 -0
  50. package/dist/lsp/lspProjectScan.js +156 -0
  51. package/dist/lsp/lspSession.js +139 -0
  52. package/dist/lsp/lspStatusReporter.js +76 -0
  53. package/dist/lsp/lspUri.js +68 -0
  54. package/dist/mcp/prompts.js +59 -0
  55. package/dist/mcp/resources.js +113 -0
  56. package/dist/mcp/roots.js +30 -0
  57. package/dist/registration/catalogue.js +123 -0
  58. package/dist/registration/extensionCollision.js +38 -0
  59. package/dist/registration/operations.js +67 -0
  60. package/dist/registration/screenshotResponse.js +71 -0
  61. package/dist/registration/toolDispatch.js +76 -0
  62. package/dist/registration/toolMeta.js +104 -0
  63. package/dist/registration/toolRefs.js +48 -0
  64. package/dist/registration/toolRegistry.js +211 -0
  65. package/dist/registry.js +291 -0
  66. package/dist/registryLiveness.js +113 -0
  67. package/dist/security/pathGuard.js +97 -0
  68. package/dist/security/profiles.js +104 -0
  69. package/dist/security/untrusted.js +23 -0
  70. package/dist/shared/errorContract.js +154 -0
  71. package/dist/shared/errors.js +21 -0
  72. package/dist/shared/pagination.js +135 -0
  73. package/dist/shared/schemaCoercion.js +173 -0
  74. package/dist/shared/stableJson.js +27 -0
  75. package/dist/shared/types.js +1 -0
  76. package/dist/shared/version.js +82 -0
  77. package/dist/startup/cliArgs.js +103 -0
  78. package/dist/startup/configReload.js +57 -0
  79. package/dist/startup/hooks.js +89 -0
  80. package/dist/startup/lifecycle.js +22 -0
  81. package/dist/startup/portConfig.js +127 -0
  82. package/dist/startup/reconcile.js +81 -0
  83. package/dist/startup/registrars.js +59 -0
  84. package/dist/startup/serverMode.js +19 -0
  85. package/dist/startup/startupEnv.js +142 -0
  86. package/dist/tools/animation.js +88 -0
  87. package/dist/tools/asset.js +58 -0
  88. package/dist/tools/assetWrite.js +16 -0
  89. package/dist/tools/audio.js +40 -0
  90. package/dist/tools/classdb.js +43 -0
  91. package/dist/tools/collision.js +23 -0
  92. package/dist/tools/debug.js +46 -0
  93. package/dist/tools/diff.js +18 -0
  94. package/dist/tools/editor.js +222 -0
  95. package/dist/tools/file.js +38 -0
  96. package/dist/tools/folder.js +27 -0
  97. package/dist/tools/inputMap.js +54 -0
  98. package/dist/tools/layerNames.js +29 -0
  99. package/dist/tools/lsp.js +524 -0
  100. package/dist/tools/navigation.js +24 -0
  101. package/dist/tools/node.js +145 -0
  102. package/dist/tools/nodeManagement.js +82 -0
  103. package/dist/tools/particles.js +83 -0
  104. package/dist/tools/path.js +30 -0
  105. package/dist/tools/playtest.js +99 -0
  106. package/dist/tools/procedural.js +101 -0
  107. package/dist/tools/resource.js +47 -0
  108. package/dist/tools/runtime.js +338 -0
  109. package/dist/tools/save.js +64 -0
  110. package/dist/tools/scene.js +102 -0
  111. package/dist/tools/sceneInheritance.js +19 -0
  112. package/dist/tools/sceneQuery.js +38 -0
  113. package/dist/tools/script.js +75 -0
  114. package/dist/tools/signals.js +53 -0
  115. package/dist/tools/sound.js +31 -0
  116. package/dist/tools/spatial.js +41 -0
  117. package/dist/tools/spriteframes.js +94 -0
  118. package/dist/tools/texture.js +35 -0
  119. package/dist/tools/theme.js +35 -0
  120. package/dist/tools/threeD.js +116 -0
  121. package/dist/tools/tilemap.js +51 -0
  122. package/dist/tools/tileset.js +226 -0
  123. package/dist/transport/authHandshake.js +43 -0
  124. package/dist/transport/bridge.js +225 -0
  125. package/dist/transport/channel.js +352 -0
  126. package/dist/transport/heartbeat.js +54 -0
  127. package/dist/transport/runtimeConnection.js +239 -0
  128. package/dist/transport/tokenPath.js +98 -0
  129. package/package.json +97 -4
@@ -0,0 +1,81 @@
1
+ import { MODULE_ALLOWED } from "./serverMode.js";
2
+ import { isReadOnly } from "../security/profiles.js";
3
+ import * as registrars from "./registrars.js";
4
+ import { readMcpJsonEnv, applyEnvUpdate } from "./configReload.js";
5
+ import { batchToolRegistration } from "../registration/toolRegistry.js";
6
+ import { removeAllToolRefs, toolRefCount } from "../registration/toolRefs.js";
7
+ import { resetLoadedGroups } from "../groups/groups.js";
8
+ /**
9
+ * Construct the reconciler. discover is injected (the ExtensionManager's
10
+ * discoverExtensions) so this module does not import extensions.ts — keeping the
11
+ * dependency graph acyclic and the construction order linear.
12
+ */
13
+ export function createReconciler(deps) {
14
+ const { server, bridge, projectPath, discover } = deps;
15
+ // Debounce config_reloaded to prevent rapid config changes from causing
16
+ // overlapping remove+rebuild cycles that leave the tool list empty.
17
+ let configReloadTimer = undefined;
18
+ // One-shot latch for the startup reconcile. Set by armStartupReconcile,
19
+ // cleared on the first effective reconcile.
20
+ let needsStartupReconcile = false;
21
+ function removeAllTools() {
22
+ removeAllToolRefs();
23
+ resetLoadedGroups();
24
+ }
25
+ function handleConfigReload() {
26
+ // Re-read .mcp.json for env changes (READ_ONLY, limits, etc.).
27
+ const newEnv = readMcpJsonEnv(projectPath);
28
+ if (newEnv) {
29
+ applyEnvUpdate(newEnv);
30
+ }
31
+ // Collapse the remove+rebuild into a SINGLE tools/list_changed. The SDK
32
+ // auto-emits on every ref.remove() (in removeAllTools) and every registerTool
33
+ // (in registrars.registerBuiltinModules/registerGroups); batching suppresses those per-op
34
+ // notifications and fires exactly one when the rebuild completes, so the
35
+ // client never observes the transient empty/partial tool list mid-reload.
36
+ batchToolRegistration(server, () => {
37
+ removeAllTools();
38
+ registrars.registerBuiltinModules(server, bridge, MODULE_ALLOWED);
39
+ registrars.registerGroups(server, bridge, isReadOnly());
40
+ });
41
+ process.stderr.write(`[godot-mcp] config reloaded — ${toolRefCount()} tools registered\n`);
42
+ // Re-discover extensions + register discover_tools. Runs AFTER and OUTSIDE
43
+ // the batch above: it is async (batchToolRegistration is synchronous and would
44
+ // restore sendToolListChanged before the async work ran), and it batches its
45
+ // bulk (ungrouped) extension registrations internally — so it reconciles the
46
+ // extension surface with only a few discrete notifications (the ungrouped
47
+ // batch, the discover_tools update, the extensions_refresh re-add), never a
48
+ // per-registration burst.
49
+ discover().catch(() => { });
50
+ }
51
+ function scheduleConfigReload() {
52
+ if (configReloadTimer)
53
+ clearTimeout(configReloadTimer);
54
+ configReloadTimer = setTimeout(() => {
55
+ configReloadTimer = undefined;
56
+ handleConfigReload();
57
+ }, 300);
58
+ }
59
+ // ── Startup reconcile ──────────────────────────────────
60
+ // The eagerly-registered tool surface is INCOMPLETE when the Godot version was
61
+ // unknown at eager registration (version-gated tools like scene_close were
62
+ // filtered out) or extension discovery timed out (extension tools never
63
+ // registered) — the classic server-before-editor cold start. Complete it
64
+ // EXACTLY ONCE: immediately if the version is already known (slow/timed-out
65
+ // discovery), or when it resolves later via the version-resolved hook
66
+ // (server-before-editor). In the common dogfood case the surface is already
67
+ // complete (version pre-populated + discovery succeeded) → needsStartupReconcile
68
+ // is false → this is a strict no-op: no reconcile, no extra tools/list_changed.
69
+ function armStartupReconcile(opts) {
70
+ needsStartupReconcile = opts.versionNullAtEagerRegistration || opts.extDiscoveryTimedOut;
71
+ function maybeStartupReconcile() {
72
+ if (!needsStartupReconcile || bridge.getGodotVersion() == null)
73
+ return;
74
+ needsStartupReconcile = false; // one-shot; also dedups reconnect re-deliveries
75
+ handleConfigReload(); // re-register (known version) + re-discover, already batched
76
+ }
77
+ maybeStartupReconcile(); // version already known (slow/timed-out discovery)
78
+ bridge.onGodotVersionKnown(maybeStartupReconcile); // version known later (server-before-editor)
79
+ }
80
+ return { handleConfigReload, scheduleConfigReload, armStartupReconcile };
81
+ }
@@ -0,0 +1,59 @@
1
+ import { registerGroupSystem } from "../groups/groups.js";
2
+ import * as animation from "../tools/animation.js";
3
+ import * as asset from "../tools/asset.js";
4
+ import * as diff from "../tools/diff.js";
5
+ import * as editor from "../tools/editor.js";
6
+ import * as file from "../tools/file.js";
7
+ import * as folder from "../tools/folder.js";
8
+ import * as inputMap from "../tools/inputMap.js";
9
+ import * as node from "../tools/node.js";
10
+ import * as playtest from "../tools/playtest.js";
11
+ import * as resource from "../tools/resource.js";
12
+ import * as runtime from "../tools/runtime.js";
13
+ import * as scene from "../tools/scene.js";
14
+ import * as script from "../tools/script.js";
15
+ import * as signal from "../tools/signals.js";
16
+ import * as save from "../tools/save.js";
17
+ import * as tilemap from "../tools/tilemap.js";
18
+ import * as tileset from "../tools/tileset.js";
19
+ import * as classdb from "../tools/classdb.js";
20
+ import * as nodeManagement from "../tools/nodeManagement.js";
21
+ import * as sceneQuery from "../tools/sceneQuery.js";
22
+ import * as spatial from "../tools/spatial.js";
23
+ import * as texture from "../tools/texture.js";
24
+ import * as sound from "../tools/sound.js";
25
+ /** Register every built-in tool module onto the server (scene, node, script, … sound — 23 modules). */
26
+ export function registerBuiltinModules(server, bridge, moduleAllowed) {
27
+ scene.register(server, bridge, moduleAllowed);
28
+ node.register(server, bridge, moduleAllowed);
29
+ script.register(server, bridge, moduleAllowed);
30
+ editor.register(server, bridge, moduleAllowed);
31
+ resource.register(server, bridge, moduleAllowed);
32
+ folder.register(server, bridge, moduleAllowed);
33
+ diff.register(server, bridge, moduleAllowed);
34
+ playtest.register(server, bridge, moduleAllowed);
35
+ tilemap.register(server, bridge, moduleAllowed);
36
+ tileset.register(server, bridge, moduleAllowed);
37
+ asset.register(server, bridge, moduleAllowed);
38
+ runtime.register(server, bridge, moduleAllowed);
39
+ signal.register(server, bridge, moduleAllowed);
40
+ animation.register(server, bridge, moduleAllowed);
41
+ inputMap.register(server, bridge, moduleAllowed);
42
+ file.register(server, bridge, moduleAllowed);
43
+ save.register(server, bridge, moduleAllowed);
44
+ classdb.register(server, bridge, moduleAllowed);
45
+ nodeManagement.register(server, bridge, moduleAllowed);
46
+ sceneQuery.register(server, bridge, moduleAllowed);
47
+ spatial.register(server, bridge, moduleAllowed);
48
+ texture.register(server, bridge, moduleAllowed);
49
+ sound.register(server, bridge, moduleAllowed);
50
+ }
51
+ /** Register the discover_tools group system (idempotent; re-callable to refresh its description). */
52
+ export function registerGroups(server, bridge, readOnly) {
53
+ // Register discover_tools with built-in groups BEFORE transport
54
+ // connects, so it's in the initial tools/list response — no extra
55
+ // notification needed for the common case (no extensions).
56
+ // If extensions are later discovered, registerGroupSystem is called
57
+ // again (idempotent) which updates the description.
58
+ registerGroupSystem(server, bridge, readOnly);
59
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Server mode — the static registration inputs.
3
+ *
4
+ * Read-only state is read live via profiles.isReadOnly() at each use site: the
5
+ * env mutates only through applyEnvUpdate (config-reload), called synchronously
6
+ * before any read, so a live read is always current — no cache needed. The
7
+ * module-allowed set (eager tools minus group-managed tools) is constant
8
+ * across reloads (resolveAllowedTools() is the fixed EAGER_TOOLS set;
9
+ * GROUP_TOOL_NAMES is immutable), so it is computed once here.
10
+ */
11
+ import { resolveAllowedTools } from "../security/profiles.js";
12
+ import { GROUP_TOOL_NAMES } from "../groups/groups.js";
13
+ /** Eager tools minus group-managed tools — the set passed to registerBuiltinModules. Constant across reloads. */
14
+ export const MODULE_ALLOWED = (() => {
15
+ const mod = new Set(resolveAllowedTools());
16
+ for (const name of GROUP_TOOL_NAMES)
17
+ mod.delete(name);
18
+ return mod;
19
+ })();
@@ -0,0 +1,142 @@
1
+ // ── Startup environment preflight ────────────────────────────────────
2
+ //
3
+ // One-shot boot-environment resolution + validation for the composition
4
+ // root (index.ts). Each function here is a stateless preflight: it reads
5
+ // process.env / the static catalogue, emits stderr (or stdout) diagnostics,
6
+ // and — for the exit gates — may process.exit. No subsystem state lives
7
+ // here; the root calls these in sequence before constructing the bridge.
8
+ // Port resolution lives in its own collaborator (portConfig.ts).
9
+ import { ALL_TOOL_DEFS, META_TOOL_NAMES } from "../registration/catalogue.js";
10
+ import { countBuiltinOperations } from "../registration/operations.js";
11
+ import { GROUP_TOOL_NAMES, GROUPS } from "../groups/groups.js";
12
+ import { MODULE_ALLOWED } from "./serverMode.js";
13
+ import { formatHelp } from "./cliArgs.js";
14
+ /** Hard-exit (code 1) if the Node runtime is below the engines.node floor (>=22). */
15
+ export function enforceNodeVersion() {
16
+ const [nodeMajor] = process.versions.node.split(".").map(Number);
17
+ if (nodeMajor < 22) {
18
+ process.stderr.write(`[godot-mcp] Error: requires Node.js >= 22 (found ${process.version}).\n` +
19
+ `Download the latest LTS from https://nodejs.org\n`);
20
+ process.exit(1);
21
+ }
22
+ }
23
+ /**
24
+ * Build the static built-in tool manifest by name, deterministically sorted.
25
+ *
26
+ * The eager set is derived from `MODULE_ALLOWED` — the same set index.ts hands to
27
+ * `registerBuiltinModules` — so the manifest reflects the names registered
28
+ * eagerly rather than a parallel list. Meta names come from `META_TOOL_NAMES` and
29
+ * group membership from `GROUPS`, the same sources `--tools-count` counts.
30
+ *
31
+ * @returns the manifest; every array and every `groups` key ascending-sorted so
32
+ * the serialization is byte-stable across turns
33
+ * @remarks Names only, and only the static built-in surface — per-project
34
+ * extension tools are dynamic and excluded, matching `--tools-count`.
35
+ */
36
+ export function buildEagerManifest() {
37
+ const eager = [...MODULE_ALLOWED].sort();
38
+ const meta = [...META_TOOL_NAMES].sort();
39
+ const groups = {};
40
+ for (const group of [...GROUPS].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
41
+ groups[group.name] = [...group.tools].sort();
42
+ }
43
+ return { eager, meta, groups };
44
+ }
45
+ /** Print the eager+meta tool manifest as pretty JSON to stdout — a pre-transport
46
+ * CLI report. Names only; a clean early-exit emit before any MCP wiring. */
47
+ function printEagerList() {
48
+ process.stdout.write(JSON.stringify(buildEagerManifest(), null, 2) + "\n");
49
+ }
50
+ /** Print the static tool-count summary to stdout — a pre-transport CLI report,
51
+ * derived from the canonical ALL_TOOL_DEFS (excludes dynamic extension tools). */
52
+ function printToolCount() {
53
+ const total = ALL_TOOL_DEFS.length;
54
+ const onDemand = GROUP_TOOL_NAMES.size;
55
+ const eager = total - onDemand;
56
+ process.stdout.write(`Total tools: ${total}\n` +
57
+ ` Eager: ${eager}\n` +
58
+ ` On-demand: ${onDemand}\n` +
59
+ `Meta: ${META_TOOL_NAMES.length} (also eager — always in tools/list)\n` +
60
+ `Groups: ${GROUPS.length}\n` +
61
+ `Operations (built-in): ${countBuiltinOperations(ALL_TOOL_DEFS)}\n` +
62
+ `Startup surface (eager + meta): ${eager + META_TOOL_NAMES.length}\n`);
63
+ }
64
+ /**
65
+ * Apply the CLI meta gates that print-and-exit before any bridge/WebSocket/
66
+ * transport setup (all editor-independent):
67
+ * - `--help` → usage on **stdout**, exit 0 (a CLI invocation, not an MCP session)
68
+ * - a parse error → the message + usage on **stderr**, exit 1 (fail loud)
69
+ * - `--tools-count` → the static count on **stdout**, exit 0
70
+ * - `--list-eager` → the static tool manifest as JSON on **stdout**, exit 0
71
+ *
72
+ * No-op when argv carries none of these. `--help` wins over a parse error so a
73
+ * bad flag alongside `--help` still prints usage rather than erroring.
74
+ */
75
+ export function applyCliMetaGates(cli) {
76
+ if (cli.help) {
77
+ process.stdout.write(formatHelp());
78
+ process.exit(0);
79
+ }
80
+ if (cli.error) {
81
+ process.stderr.write(`[godot-mcp] ${cli.error}\n\n${formatHelp()}`);
82
+ process.exit(1);
83
+ }
84
+ if (cli.toolsCount) {
85
+ printToolCount();
86
+ process.exit(0);
87
+ }
88
+ if (cli.listEager) {
89
+ printEagerList();
90
+ process.exit(0);
91
+ }
92
+ }
93
+ // Defaults match the plugin-side ProjectSettings defaults.
94
+ const SCRIPT_READ_LIMIT_DEFAULT = 262144; // 256 KB
95
+ const WS_BUFFER_LIMIT_DEFAULT = 1048576; // 1 MB
96
+ const SCRIPT_READ_LIMIT_FLOOR = 65536; // 64 KB
97
+ const WS_BUFFER_LIMIT_FLOOR = 262144; // 256 KB
98
+ function parseCapEnv(envName, defaultVal, floor) {
99
+ const raw = process.env[envName];
100
+ if (!raw)
101
+ return defaultVal;
102
+ const parsed = Number(raw);
103
+ if (!Number.isFinite(parsed) || parsed <= 0) {
104
+ process.stderr.write(`[godot-mcp] WARNING: ${envName}=${raw} is not a valid positive number; using default ${defaultVal}\n`);
105
+ return defaultVal;
106
+ }
107
+ if (parsed < floor) {
108
+ process.stderr.write(`[godot-mcp] WARNING: ${envName}=${parsed} is below minimum ${floor}; clamping to ${floor}\n`);
109
+ return floor;
110
+ }
111
+ return parsed;
112
+ }
113
+ /** Parse + clamp GODOT_MCP_SCRIPT_READ_LIMIT / _WS_BUFFER_LIMIT against their floors/defaults. */
114
+ export function resolveResponseCaps() {
115
+ const scriptReadLimitBytes = parseCapEnv("GODOT_MCP_SCRIPT_READ_LIMIT", SCRIPT_READ_LIMIT_DEFAULT, SCRIPT_READ_LIMIT_FLOOR);
116
+ const wsBufferLimitBytes = parseCapEnv("GODOT_MCP_WS_BUFFER_LIMIT", WS_BUFFER_LIMIT_DEFAULT, WS_BUFFER_LIMIT_FLOOR);
117
+ return { scriptReadLimitBytes, wsBufferLimitBytes };
118
+ }
119
+ // ── Config version check ─────────────────────────────────────────────
120
+ const EXPECTED_CONFIG_VERSION = 1;
121
+ /** Warn (stderr) if GODOT_MCP_CONFIG_VERSION is missing / non-numeric / older / newer than EXPECTED. */
122
+ export function warnConfigVersion() {
123
+ const rawConfigVersion = process.env.GODOT_MCP_CONFIG_VERSION;
124
+ if (rawConfigVersion == null || rawConfigVersion === "") {
125
+ process.stderr.write("[godot-mcp] WARNING: no GODOT_MCP_CONFIG_VERSION in env. " +
126
+ "Config may be from a pre-release build — regenerate .mcp.json from the toolkit dock.\n");
127
+ }
128
+ else {
129
+ const configVersion = Number(rawConfigVersion);
130
+ if (!Number.isFinite(configVersion)) {
131
+ process.stderr.write(`[godot-mcp] WARNING: GODOT_MCP_CONFIG_VERSION="${rawConfigVersion}" is not a valid number.\n`);
132
+ }
133
+ else if (configVersion < EXPECTED_CONFIG_VERSION) {
134
+ process.stderr.write(`[godot-mcp] WARNING: config version ${configVersion} is outdated (expected ${EXPECTED_CONFIG_VERSION}). ` +
135
+ `Regenerate .mcp.json from the toolkit dock.\n`);
136
+ }
137
+ else if (configVersion > EXPECTED_CONFIG_VERSION) {
138
+ process.stderr.write(`[godot-mcp] WARNING: config version ${configVersion} is newer than this server understands (max ${EXPECTED_CONFIG_VERSION}). ` +
139
+ `Consider updating the server (npm update).\n`);
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,88 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ export const animationTools = [
4
+ {
5
+ name: "animation_keyframe",
6
+ method: "animation.keyframe",
7
+ description: "Add/remove a keyframe on an existing animation's track. animation must already exist; action='add' auto-creates the track only. Idempotent on exact-time dup.",
8
+ inputSchema: {
9
+ action: z.enum(["add", "remove"]),
10
+ player_path: z.string(),
11
+ animation_name: z.string(),
12
+ track_path: z.string(),
13
+ time: z.coerce.number(),
14
+ value: z.unknown().optional().describe("Required for action='add'."),
15
+ track_type: z.enum(["value"]).optional().describe("Track type; only 'value' supported currently."),
16
+ },
17
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false },
18
+ operationParam: "action",
19
+ successHint: "Verify keys with animation_get_keys. Configure AnimationTree with animationtree_edit.",
20
+ },
21
+ {
22
+ name: "animation_get_keys",
23
+ method: "animation.get_keys",
24
+ description: "List keys on an AnimationPlayer track: { time, value, transition }. Read-only; no auto-track-create.",
25
+ inputSchema: {
26
+ player_path: z.string(),
27
+ animation_name: z.string(),
28
+ track_path: z.string(),
29
+ },
30
+ annotations: { readOnlyHint: true, openWorldHint: false },
31
+ },
32
+ {
33
+ name: "animationtree_edit",
34
+ method: "animationtree.edit",
35
+ description: "Configure AnimationTree state machines: set root, add/remove nodes and transitions, or set properties.",
36
+ inputSchema: {
37
+ node_path: z.string().describe("Path to an AnimationTree node in the edited scene."),
38
+ action: z
39
+ .enum(["set_root", "add_node", "remove_node", "add_transition", "remove_transition", "set_property"])
40
+ .describe("Operation to perform on the AnimationTree."),
41
+ root_type: z
42
+ .enum(["AnimationNodeStateMachine", "AnimationNodeBlendTree"])
43
+ .optional()
44
+ .describe("For set_root: type of root node to create."),
45
+ node_name: z.string().optional().describe("For add_node/remove_node: name of the state machine node."),
46
+ node_type: z
47
+ .string()
48
+ .optional()
49
+ .describe("For add_node: AnimationNode subclass (e.g. AnimationNodeAnimation, AnimationNodeBlendSpace2D)."),
50
+ animation_name: z
51
+ .string()
52
+ .optional()
53
+ .describe("For add_node with AnimationNodeAnimation: which animation to play."),
54
+ position: z
55
+ .object({ x: z.number(), y: z.number() })
56
+ .optional()
57
+ .describe("For add_node: graph position { x, y }."),
58
+ from: z.string().optional().describe("For transitions: source node name."),
59
+ to: z.string().optional().describe("For transitions: destination node name."),
60
+ switch_mode: z
61
+ .enum(["immediate", "sync", "at_end"])
62
+ .optional()
63
+ .describe("For add_transition: when the transition fires."),
64
+ advance_condition: z.string().optional().describe("For add_transition: condition name for conditional advance."),
65
+ advance_mode: z
66
+ .enum(["disabled", "enabled", "auto"])
67
+ .optional()
68
+ .describe("For add_transition: advance mode (disabled=0, enabled=1, auto=2)."),
69
+ target_node: z.string().optional().describe("For set_property: node name in the state machine."),
70
+ property: z.string().optional().describe("For set_property: property name to set."),
71
+ value: z.unknown().optional().describe("For set_property: value to assign."),
72
+ },
73
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: true },
74
+ operationParam: "action",
75
+ },
76
+ {
77
+ name: "animationtree_list",
78
+ method: "animationtree.list",
79
+ description: "List an AnimationTree state machine's structure: root type, nodes, and transitions. Read-only; no mutation.",
80
+ inputSchema: {
81
+ node_path: z.string().describe("Path to an AnimationTree node in the edited scene."),
82
+ },
83
+ annotations: { readOnlyHint: true, openWorldHint: false },
84
+ },
85
+ ];
86
+ export function register(server, bridge, allowedTools) {
87
+ registerTools(server, bridge, animationTools, allowedTools);
88
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ import { coercedBoolean, jsonCoerce } from "../shared/schemaCoercion.js";
4
+ import { PROJECT_FILE_PATH } from "../security/pathGuard.js";
5
+ import { assetWriteFields } from "./assetWrite.js";
6
+ import { paginationDoc } from "../shared/pagination.js";
7
+ export const assetTools = [
8
+ {
9
+ name: "asset_list",
10
+ method: "asset.list",
11
+ description: "Enumerate res:// assets with filters (path_prefix, name_glob, class_filter ancestry-aware, extension_filter). Returns [{path,class,modified_unix}]. " +
12
+ paginationDoc("assets", { resumable: false, cursorlessNav: "narrow filters or raise limit" }) +
13
+ "limit caps at 2000 (default 500); a request above 2000 is clamped and limit_clamped is set (a non-positive limit is rejected).",
14
+ inputSchema: {
15
+ path_prefix: z.string().optional(),
16
+ name_glob: z.string().optional(),
17
+ class_filter: z.string().optional(),
18
+ extension_filter: z.preprocess(jsonCoerce, z.array(z.string())).optional(),
19
+ limit: z.coerce.number().optional().describe("Max assets returned (default 500, clamped to 2000)"),
20
+ },
21
+ annotations: { readOnlyHint: true, openWorldHint: false },
22
+ successHint: "A just-deindexed asset (e.g. after resource_delete) can linger in this list until EditorFileSystem re-scans — call editor_refresh to force a rescan if a deleted asset still appears.",
23
+ pathParams: [{ param: "path_prefix", guard: "project" }],
24
+ },
25
+ {
26
+ name: "asset_get_dependencies",
27
+ method: "asset.get_dependencies",
28
+ description: "Forward dependencies of a res:// resource/scene via EditorFileSystem cache. include_transitive walks deps-of-deps. Returns [{path,raw_path,class}]. " +
29
+ paginationDoc("dependencies", { resumable: false, cursorlessNav: "narrow the query or raise limit" }),
30
+ inputSchema: {
31
+ file_path: z.string(),
32
+ include_transitive: coercedBoolean().optional(),
33
+ limit: z.coerce.number().optional().describe("Max dependencies returned (default 200)"),
34
+ },
35
+ annotations: { readOnlyHint: true, openWorldHint: false },
36
+ pathParams: [PROJECT_FILE_PATH],
37
+ },
38
+ {
39
+ name: "asset_import",
40
+ method: "asset.import",
41
+ description: "Import binary asset (image/audio/font/3D) into res:// via exactly one of source_path (absolute or res:// path) or base64_data. Triggers EditorFileSystem scan. if_exists:return|fail|replace.",
42
+ inputSchema: {
43
+ source_path: z.string().optional(),
44
+ base64_data: z.string().optional(),
45
+ dest_path: z.string(),
46
+ ...assetWriteFields,
47
+ },
48
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false },
49
+ successHint: "Verify import with resource_load. Check file system with asset_list.",
50
+ // Only dest_path is guarded. source_path is deliberately NOT declared: it is
51
+ // an absolute filesystem path to an external asset (the toolkit reads it raw,
52
+ // guarding only dest_path) — guarding it would false-reject intended use.
53
+ pathParams: [{ param: "dest_path", guard: "project" }],
54
+ },
55
+ ];
56
+ export function register(server, bridge, allowedTools) {
57
+ registerTools(server, bridge, assetTools, allowedTools);
58
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Shared file-write fields for the tools that write a res:// asset and settle
4
+ * its import through the toolkit's `write_asset_with_settle` bracket:
5
+ * asset_import, texture_generate, sound_generate.
6
+ *
7
+ * The path parameter itself is declared per-tool (asset_import uses `dest_path`;
8
+ * the generators use `path`), so only the two identical fields live here.
9
+ *
10
+ * @internal Folder-internal to `tools/` — shared only among the sibling
11
+ * asset-write tool modules, not part of the public surface.
12
+ */
13
+ export const assetWriteFields = {
14
+ if_exists: z.enum(["return", "fail", "replace"]).optional(),
15
+ wait_for_scan_ms: z.coerce.number().optional(),
16
+ };
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ export const audioTools = [
4
+ {
5
+ name: "audiobus_edit",
6
+ method: "audiobus.edit",
7
+ description: "Manage audio buses: add/remove buses, set volume/send/solo/mute, add/remove effects. bus_name takes priority over bus_index. Effect type: full class (AudioEffectReverb) or suffix (Reverb).",
8
+ inputSchema: {
9
+ action: z.enum(["add_bus", "remove_bus", "set_bus", "add_effect", "remove_effect"]).describe("Bus operation"),
10
+ bus_name: z.string().optional().describe("Bus name"),
11
+ bus_index: z.number().int().optional().describe("Bus index (alternative to name)"),
12
+ send_to: z.string().optional().describe("Parent bus name (default: Master)"),
13
+ volume_db: z.number().optional().describe("Volume in dB"),
14
+ solo: z.boolean().optional().describe("Solo this bus"),
15
+ mute: z.boolean().optional().describe("Mute this bus"),
16
+ effect: z
17
+ .object({
18
+ type: z.string().describe("Effect class: Reverb, Delay, Compressor, Chorus, EQ, etc."),
19
+ index: z.number().int().optional().describe("Effect slot index"),
20
+ enabled: z.boolean().optional().describe("Enable/disable the effect"),
21
+ properties: z.record(z.string(), z.unknown()).optional().describe("Effect-specific properties"),
22
+ })
23
+ .optional()
24
+ .describe("Effect to add/remove"),
25
+ },
26
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true },
27
+ operationParam: "action",
28
+ successHint: "Effect types must be full class names (e.g., AudioEffectReverb not Reverb).",
29
+ },
30
+ {
31
+ name: "audiobus_list",
32
+ method: "audiobus.list",
33
+ description: "List all audio buses with volume/send/solo/mute and per-bus effects. Read-only snapshot of the bus layout.",
34
+ inputSchema: {},
35
+ annotations: { readOnlyHint: true, openWorldHint: false },
36
+ },
37
+ ];
38
+ export function register(server, bridge, allowedTools) {
39
+ registerTools(server, bridge, audioTools, allowedTools);
40
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ import { coercedBoolean } from "../shared/schemaCoercion.js";
4
+ import { offsetLimitParams, paginationDoc } from "../shared/pagination.js";
5
+ export const classdbTools = [
6
+ {
7
+ name: "classdb_get_info",
8
+ method: "classdb.get_info",
9
+ description: "Inspect any Godot class: properties, methods, signals, constants, inheritance. Supports engine + user class_name classes. " +
10
+ paginationDoc("<section>", { resumable: true }) +
11
+ "The envelope is per-section (properties, methods, signals, constants); offset and limit apply within each section (limit default 200, clamped above 200).",
12
+ inputSchema: {
13
+ class_name: z.string().describe("Engine class (e.g. RigidBody3D) or user-defined class_name."),
14
+ include_inherited: coercedBoolean()
15
+ .optional()
16
+ .describe("Include inherited members (default: false, own class only)."),
17
+ sections: z
18
+ .array(z.enum(["properties", "methods", "signals", "constants"]))
19
+ .optional()
20
+ .describe("Which sections to return (default: all). Limit to reduce token cost."),
21
+ ...offsetLimitParams,
22
+ },
23
+ annotations: { readOnlyHint: true, openWorldHint: false },
24
+ },
25
+ {
26
+ name: "classdb_search",
27
+ method: "classdb.search",
28
+ description: "Find Godot classes by inheritance and/or name pattern. Returns class list with parent + instantiability. " +
29
+ paginationDoc("classes", { resumable: true }) +
30
+ "limit default 200, clamped above 200.",
31
+ inputSchema: {
32
+ base_class: z.string().optional().describe("Filter to subclasses of this class."),
33
+ pattern: z.string().optional().describe("Case-insensitive substring match on class name."),
34
+ instantiable_only: coercedBoolean().optional().describe("Exclude abstract classes (default: true)."),
35
+ include_global: coercedBoolean().optional().describe("Include user class_name classes (default: true)."),
36
+ ...offsetLimitParams,
37
+ },
38
+ annotations: { readOnlyHint: true, openWorldHint: false },
39
+ },
40
+ ];
41
+ export function register(server, bridge, allowedTools) {
42
+ registerTools(server, bridge, classdbTools, allowedTools);
43
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ export const collisionTools = [
4
+ {
5
+ name: "collision_from_texture",
6
+ method: "node.collision_from_sprite",
7
+ description: "Auto-generate CollisionPolygon2D from a Sprite2D's texture alpha. Uses BitMap to trace opaque regions. For platformer terrain, character hitboxes, irregular shapes.",
8
+ inputSchema: {
9
+ sprite_path: z.string().describe("Path to a Sprite2D/TextureRect node with a texture"),
10
+ parent_path: z.string().optional().describe("Parent for the new CollisionPolygon2D (default: sprite's parent)"),
11
+ target_name: z.string().optional().describe("Name for the CollisionPolygon2D (default: {sprite}_collision)"),
12
+ simplification: z
13
+ .number()
14
+ .optional()
15
+ .describe("Polygon simplification epsilon 0.0-10.0 (default 2.0, higher=fewer points)"),
16
+ },
17
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
18
+ successHint: "Assign the generated polygon to a CollisionPolygon2D via node_set_property.",
19
+ },
20
+ ];
21
+ export function register(server, bridge, allowedTools) {
22
+ registerTools(server, bridge, collisionTools, allowedTools);
23
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Debugger tool definitions — breakpoint management + debug state
3
+ * via the toolkit's EditorDebuggerPlugin bridge. All 4 tools are
4
+ * group-only (lazy-loaded via discover_tools → debugger group).
5
+ */
6
+ import { z } from "zod";
7
+ import { coercedBoolean } from "../shared/schemaCoercion.js";
8
+ export const debugTools = [
9
+ {
10
+ name: "debug_state",
11
+ method: "debug.state",
12
+ description: "Check debugger status: is a debug session active, is it paused at a breakpoint, can it be debugged. No params.",
13
+ inputSchema: {},
14
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
15
+ },
16
+ {
17
+ name: "debug_list_breakpoints",
18
+ method: "debug.list_breakpoints",
19
+ description: "List all GDScript breakpoints currently set in the script editor. Returns file paths and line numbers. .gd only.",
20
+ inputSchema: {},
21
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
22
+ },
23
+ {
24
+ name: "debug_set_breakpoint",
25
+ method: "debug.set_breakpoint",
26
+ description: "Set or clear a breakpoint at a specific file and line in the script editor. GDScript (.gd) files only.",
27
+ inputSchema: {
28
+ file_path: z.string().describe("res:// path to a .gd file (e.g. res://scripts/player.gd)"),
29
+ line: z.coerce.number().int().describe("1-based line number"),
30
+ // Optional with `.optional()` OUTERMOST: a coercion wrapper (preprocess =
31
+ // ZodPipe) does not inherit an inner `.optional()` under the SDK's
32
+ // io:"input" conversion, which would flip the param to `required` in
33
+ // tools/list. The plugin defaults enabled→true (debug_commands.gd), so no
34
+ // server-side .default is needed. A structural test guards against regression.
35
+ enabled: coercedBoolean().optional().describe("true to set, false to clear (default true)"),
36
+ },
37
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
38
+ },
39
+ {
40
+ name: "debug_continue",
41
+ method: "debug.continue",
42
+ description: "Resume execution when the debugger is paused at a breakpoint. Fails if not breaked.",
43
+ inputSchema: {},
44
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
45
+ },
46
+ ];
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ // Line-based JSON diff between two scene-tree snapshots. Useful for
4
+ // "what changed since I last looked?" workflows after a sequence of
5
+ // mutations. The plugin does the heavy lifting; this module is a thin
6
+ // pass-through.
7
+ export const diffTools = [
8
+ {
9
+ name: "scene_diff",
10
+ method: "scene.diff",
11
+ description: "Compare a prior scene-tree snapshot against another snapshot (or current edited scene if 'after' omitted). Returns { changed, diff, added, removed }.",
12
+ inputSchema: { before: z.any(), after: z.any().optional() },
13
+ annotations: { readOnlyHint: true, openWorldHint: false },
14
+ },
15
+ ];
16
+ export function register(server, bridge, allowedTools) {
17
+ registerTools(server, bridge, diffTools, allowedTools);
18
+ }