@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,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
+ }