@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,145 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ export const nodeTools = [
4
+ {
5
+ name: "node_get_property",
6
+ method: "node.get_property",
7
+ description: "Read a property from the node at path. Returns { value } (engine types are dict-wrapped).",
8
+ inputSchema: {
9
+ node_path: z.string(),
10
+ property: z
11
+ .string()
12
+ .describe("Property name. Use ':' to chain into sub-resources (e.g. 'material:shader_parameter/value')."),
13
+ },
14
+ annotations: { readOnlyHint: true, openWorldHint: false },
15
+ successHint: "For available properties use node_get_property_list. For tree-wide overview use scene_get_tree with include_properties.",
16
+ },
17
+ // Deliberately detailed description: spelling out the editor-time vs runtime
18
+ // distinction and the anchors-preset pitfall materially reduces mis-calls.
19
+ {
20
+ name: "node_set_property",
21
+ method: "node.set_property",
22
+ description: "Set a property on a node in the EDITOR scene tree (saved to .tscn files). " +
23
+ "Does NOT affect the running game — for runtime property changes during playtesting, use runtime_set_property.\n\n" +
24
+ 'Node paths are relative to the edited scene root: "." is root, "./Player" is a direct child, "./Player/Sprite2D" for deeper nodes.\n\n' +
25
+ "Engine types: {type:'Vector2',x,y}. Inline sub-resources: {type:'NewResource',class:'CircleShape2D',properties:{radius:50}}. " +
26
+ "External resources (textures, audio, tilesets, materials): {type:'Resource', path:'res://path/to/file.tres'}. " +
27
+ "Packed arrays: {type:'PackedVector2Array', values:[{type:'Vector2',x:0,y:0}, ...]}. " +
28
+ "Collision layers: {type:'LayerMask', layers:[1,4,6]} (by number) or {type:'LayerMask', layers:['player','walls']} (by name from layer_names_set); optional category defaults to '2d_physics'. " +
29
+ "All supported type tags: Vector2, Vector3, Vector4, Vector2i, Vector3i, Color, Rect2, Rect2i, " +
30
+ "Transform2D, Transform3D, NodePath, Resource, NewResource, PackedVector2Array, PackedVector3Array, PackedColorArray, LayerMask. " +
31
+ "Unknown type tags are rejected with an error listing supported types.\n\n" +
32
+ "Anchor presets: setting anchors_preset alone may not auto-apply underlying values. " +
33
+ "For reliable layout, set anchor_left/top/right/bottom and offset_left/top/right/bottom explicitly.\n\n" +
34
+ "Batch mode: pass batch:[{node_path, property, value, make_unique?}, ...] to set multiple properties at once.",
35
+ inputSchema: {
36
+ node_path: z.string().optional().describe("Single mode: path to target node"),
37
+ property: z
38
+ .string()
39
+ .optional()
40
+ .describe("Single mode: property name. Compound '/' paths supported. Use ':' for sub-resource chaining (e.g. 'material:shader_parameter/value')."),
41
+ value: z.unknown().optional(),
42
+ make_unique: z
43
+ .boolean()
44
+ .optional()
45
+ .describe("When true and the compound path targets an external (.tres) sub-resource, " +
46
+ "auto-duplicate it as an inline copy before setting. " +
47
+ "Equivalent to the Inspector's 'Make Unique'. Only needed for compound paths on external resources."),
48
+ batch: z
49
+ .array(z.object({
50
+ node_path: z.string(),
51
+ property: z.string(),
52
+ value: z.unknown(),
53
+ make_unique: z
54
+ .boolean()
55
+ .optional()
56
+ .describe("Per-entry make_unique — auto-duplicate external sub-resource as inline copy."),
57
+ }))
58
+ .optional()
59
+ .describe("Batch mode: array of {node_path, property, value, make_unique?}. " +
60
+ "Omit for single-property operations. " +
61
+ "When present, top-level node_path/property/value are ignored."),
62
+ },
63
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
64
+ successHint: "Save changes with editor_save_scene. For batch edits, use the batch array in a single call.",
65
+ },
66
+ {
67
+ name: "node_get_property_list",
68
+ method: "node.get_property_list",
69
+ description: "Introspect node properties. mask: common (default), all, groups, script. 'script' returns all script variables with public/private label; use visibility param to filter.",
70
+ inputSchema: {
71
+ node_path: z.string(),
72
+ mask: z
73
+ .enum(["common", "all", "groups", "script"])
74
+ .optional()
75
+ .describe("Property filter. 'common' (default) returns 8-12 most-edited. 'all' returns full list. 'groups' returns names+usage only. 'script' returns script variables with visibility label. Prefer 'common' or node_get_property; 'all' returns the full list and is large."),
76
+ visibility: z
77
+ .enum(["public", "private", "all"])
78
+ .optional()
79
+ .default("all")
80
+ .describe("Filter for mask='script'. 'public' = no _ prefix, 'private' = _ prefix, 'all' = both."),
81
+ },
82
+ annotations: { readOnlyHint: true, openWorldHint: false },
83
+ successHint: "For specific values use node_get_property. Filter with mask parameter (common/all/groups/script).",
84
+ },
85
+ {
86
+ name: "node_set_script",
87
+ method: "node.set_script",
88
+ description: "Attach a script (.gd/.cs) to a node. Returns @export properties exposed by the script. Empty script_path string detaches.",
89
+ inputSchema: {
90
+ node_path: z.string(),
91
+ script_path: z.string(),
92
+ },
93
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
94
+ successHint: "Write script content with script_write. View @export properties in the returned property list.",
95
+ // script_path is a res:// file path; node_path is a scene-tree path (not guarded).
96
+ // Empty script_path (detach) is skipped by checkPathGuard, deferring to the toolkit.
97
+ pathParams: [{ param: "script_path", guard: "project" }],
98
+ },
99
+ {
100
+ name: "control_set_layout",
101
+ method: "control.set_layout",
102
+ description: "Set anchor preset + optional margins on a Control node in one call. Uses set_anchors_and_offsets_preset(). Returns final_rect.",
103
+ inputSchema: {
104
+ node_path: z.string(),
105
+ preset: z
106
+ .string()
107
+ .describe("Layout preset: PRESET_TOP_LEFT, PRESET_TOP_RIGHT, PRESET_BOTTOM_LEFT, PRESET_BOTTOM_RIGHT, " +
108
+ "PRESET_CENTER_LEFT, PRESET_CENTER_TOP, PRESET_CENTER_RIGHT, PRESET_CENTER_BOTTOM, " +
109
+ "PRESET_CENTER, PRESET_LEFT_WIDE, PRESET_TOP_WIDE, PRESET_RIGHT_WIDE, PRESET_BOTTOM_WIDE, " +
110
+ "PRESET_VCENTER_WIDE, PRESET_HCENTER_WIDE, PRESET_FULL_RECT"),
111
+ resize_mode: z
112
+ .enum(["keep_size", "set_to_anchors"])
113
+ .optional()
114
+ .describe("keep_size (default) preserves size; set_to_anchors resizes to anchor region."),
115
+ margins: z
116
+ .object({
117
+ left: z.coerce.number().optional(),
118
+ right: z.coerce.number().optional(),
119
+ top: z.coerce.number().optional(),
120
+ bottom: z.coerce.number().optional(),
121
+ })
122
+ .optional()
123
+ .describe("Additive offsets applied after the preset (in pixels)."),
124
+ },
125
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
126
+ successHint: "Margins are applied AFTER the preset. For Container children, layout is overridden by the parent.",
127
+ },
128
+ // node_call_method can call arbitrary methods — risk communicated via
129
+ // destructiveHint annotation. Agent-side filtering recommended for
130
+ // untrusted contexts (see security-recommendations.md).
131
+ {
132
+ name: "node_call_method",
133
+ method: "node.call_method",
134
+ description: "Call method with args on an edited-scene node (editor-only; for runtime nodes use execute_code).",
135
+ inputSchema: {
136
+ node_path: z.string(),
137
+ method_name: z.string(),
138
+ args: z.array(z.unknown()).optional(),
139
+ },
140
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true },
141
+ },
142
+ ];
143
+ export function register(server, bridge, allowedTools) {
144
+ registerTools(server, bridge, nodeTools, allowedTools);
145
+ }
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ import { coercedBoolean, jsonCoerce } from "../shared/schemaCoercion.js";
4
+ export const nodeManagementTools = [
5
+ // Deliberately detailed description: this action-consolidated tool needs
6
+ // per-action param documentation.
7
+ {
8
+ name: "node_manage",
9
+ method: "node.manage",
10
+ description: "Structural node operations on the edited scene tree.\n\n" +
11
+ "action: rename — requires new_name.\n" +
12
+ "action: reparent — requires new_parent_path, optional keep_global_transform (default true).\n" +
13
+ "action: reorder — requires new_index (0-based sibling index).\n" +
14
+ "action: duplicate — optional new_name, parent_path, properties (overrides like {position:{x,y}}).",
15
+ inputSchema: {
16
+ action: z.enum(["rename", "reparent", "reorder", "duplicate"]),
17
+ node_path: z.string(),
18
+ new_name: z.string().optional().describe("Required for rename; optional for duplicate."),
19
+ new_parent_path: z.string().optional().describe("Required for reparent."),
20
+ keep_global_transform: coercedBoolean()
21
+ .optional()
22
+ .describe("For reparent: preserve world transform. Default true."),
23
+ new_index: z.coerce.number().int().optional().describe("For reorder: 0-based sibling index."),
24
+ parent_path: z.string().optional().describe("For duplicate: target parent. Defaults to same parent."),
25
+ properties: z
26
+ .record(z.string(), z.unknown())
27
+ .optional()
28
+ .describe("For duplicate: property overrides on the copy (e.g. {position:{x:100,y:200}})."),
29
+ },
30
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
31
+ operationParam: "action",
32
+ successHint: "After rename/reparent/reorder, scripts using $Path, get_node() paths, or %UniqueNames referencing the affected node may break. Check scripts on the moved node and its immediate neighbors.",
33
+ },
34
+ {
35
+ name: "node_groups",
36
+ method: "node.groups",
37
+ description: "Manage node group membership. Groups are the idiomatic Godot way to tag and query game objects " +
38
+ "(e.g. 'coins', 'enemies').\n\n" +
39
+ "Single mode: node_path + group (node_path required for add/remove/list). " +
40
+ "Batch mode: entries array of {node_path, group} carries per-item paths, and the top-level " +
41
+ "node_path/group are ignored.\n\n" +
42
+ "action: add — requires group. action: remove — requires group. action: list — returns all groups (single only).",
43
+ inputSchema: {
44
+ action: z.enum(["add", "remove", "list"]),
45
+ node_path: z
46
+ .string()
47
+ .optional()
48
+ .describe("Single mode: target node path. Required for single add/remove/list; omit in batch mode (provide entries instead)."),
49
+ group: z.string().optional().describe("Group name. Required for single add/remove."),
50
+ persistent: coercedBoolean().optional().describe("For add: save to .tscn. Default true."),
51
+ entries: z
52
+ .preprocess(jsonCoerce, z.array(z.record(z.string(), z.unknown())))
53
+ .optional()
54
+ .describe("Batch mode (add/remove only): array of {node_path, group}. " +
55
+ "When present, processes all entries as a batch." +
56
+ "node_path and group params are ignored in batch mode."),
57
+ },
58
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
59
+ operationParam: "action",
60
+ },
61
+ {
62
+ name: "autoload_manage",
63
+ method: "autoload.manage",
64
+ description: "Manage project autoload singletons (GameManager, AudioManager, etc.). " +
65
+ "Writes to project.godot; takes effect on next game launch.\n\n" +
66
+ "action: register — requires name + script_path. action: unregister — requires name. action: list — returns all.",
67
+ inputSchema: {
68
+ action: z.enum(["register", "unregister", "list"]),
69
+ name: z.string().optional().describe("Autoload name (e.g. 'GameManager'). Required for register/unregister."),
70
+ script_path: z
71
+ .string()
72
+ .optional()
73
+ .describe("Script path (e.g. 'res://scripts/game_manager.gd'). Required for register."),
74
+ enabled: coercedBoolean().optional().describe("For register: auto-initialize on startup. Default true."),
75
+ },
76
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
77
+ operationParam: "action",
78
+ },
79
+ ];
80
+ export function register(server, bridge, allowedTools) {
81
+ registerTools(server, bridge, nodeManagementTools, allowedTools);
82
+ }
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ const vec3 = z.object({ x: z.number(), y: z.number(), z: z.number() });
3
+ const vec2 = z.object({ x: z.number(), y: z.number() });
4
+ const color = z.object({
5
+ r: z.number(),
6
+ g: z.number(),
7
+ b: z.number(),
8
+ a: z.number().optional(),
9
+ });
10
+ const rangeOrFixed = z.union([z.number(), z.object({ min: z.number(), max: z.number() })]);
11
+ const colorRampPoint = z.object({
12
+ offset: z.number(),
13
+ color,
14
+ });
15
+ const curvePoint = z.object({
16
+ x: z.number(),
17
+ y: z.number(),
18
+ });
19
+ export const particleTools = [
20
+ {
21
+ name: "particles_create",
22
+ method: "particles.create",
23
+ description: "Create GPU particle system (2D/3D) with presets: fire, smoke, sparks, rain, snow, explosion, magic, dust. Inline color_ramp/scale_curve/alpha_curve. One call replaces 7+ manual steps.",
24
+ inputSchema: {
25
+ parent_path: z.string().describe("Parent node path"),
26
+ name: z.string().optional().describe("Node name"),
27
+ type: z.enum(["2d", "3d"]).describe("GPUParticles2D or GPUParticles3D"),
28
+ preset: z
29
+ .enum(["fire", "smoke", "sparks", "rain", "snow", "explosion", "magic", "dust"])
30
+ .optional()
31
+ .describe("Effect preset (overridable with explicit params)"),
32
+ amount: z.number().int().optional().describe("Particle count"),
33
+ lifetime: z.number().optional().describe("Particle lifetime (seconds)"),
34
+ one_shot: z.boolean().optional().describe("Single burst then stop"),
35
+ explosiveness: z.number().optional().describe("0-1, burst factor"),
36
+ speed_scale: z.number().optional().describe("Simulation speed"),
37
+ local_coords: z.boolean().optional().describe("Emit in local space"),
38
+ texture_path: z.string().optional().describe("Particle texture (2D, res:// path)"),
39
+ mesh: z.enum(["quad", "box", "sphere"]).optional().describe("3D draw pass mesh"),
40
+ emission_shape: z
41
+ .enum(["point", "sphere", "sphere_surface", "box", "ring"])
42
+ .optional()
43
+ .describe("Emission shape"),
44
+ emission_sphere_radius: z.number().optional(),
45
+ emission_box_extents: vec3.optional().describe("Box emission half-extents"),
46
+ direction: vec3.optional().describe("Emission direction"),
47
+ spread: z.number().optional().describe("Spread angle (degrees, 0-180)"),
48
+ initial_velocity: rangeOrFixed.optional().describe("Initial speed (fixed or {min,max})"),
49
+ gravity: vec3.optional().describe("Gravity vector"),
50
+ damping: rangeOrFixed.optional(),
51
+ orbit_velocity: rangeOrFixed.optional(),
52
+ scale_range: rangeOrFixed.optional().describe("Particle scale"),
53
+ angle: rangeOrFixed.optional(),
54
+ angular_velocity: rangeOrFixed.optional(),
55
+ color: color.optional().describe("Flat particle color"),
56
+ hue_variation: rangeOrFixed.optional(),
57
+ color_ramp: z
58
+ .union([
59
+ z.string().describe("Path to Gradient .tres"),
60
+ z.object({
61
+ points: z.array(colorRampPoint),
62
+ interpolation: z.enum(["linear", "cubic", "constant"]).optional(),
63
+ }),
64
+ ])
65
+ .optional()
66
+ .describe("Color gradient over lifetime"),
67
+ alpha_curve: z
68
+ .union([z.string(), z.object({ points: z.array(curvePoint) })])
69
+ .optional()
70
+ .describe("Alpha curve over lifetime"),
71
+ scale_curve: z
72
+ .union([z.string(), z.object({ points: z.array(curvePoint) })])
73
+ .optional()
74
+ .describe("Scale curve over lifetime"),
75
+ turbulence_enabled: z.boolean().optional(),
76
+ turbulence_noise_strength: z.number().optional(),
77
+ particle_flag_align_y: z.boolean().optional(),
78
+ position: z.union([vec2, vec3]).optional().describe("Node position"),
79
+ },
80
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
81
+ successHint: "Customize with procedural_edit_curve (velocity/emission), procedural_edit_gradient (color over lifetime).",
82
+ },
83
+ ];
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ import { jsonCoerce } from "../shared/schemaCoercion.js";
4
+ export const pathTools = [
5
+ {
6
+ name: "path2d_edit_curve",
7
+ method: "path2d.edit_curve",
8
+ description: "Edit a Path2D node's Curve2D — set, add, or remove points with bezier control handles. For patrol routes, moving platforms, projectile curves, and camera rails.",
9
+ inputSchema: {
10
+ node_path: z.string().describe("Path2D node path in the scene tree"),
11
+ action: z
12
+ .enum(["set", "add", "remove", "clear"])
13
+ .describe("set=replace all, add=append/insert, remove=delete at index, clear=remove all"),
14
+ points: z
15
+ .preprocess(jsonCoerce, z.array(z.object({
16
+ position: z.object({ x: z.number(), y: z.number() }),
17
+ in_handle: z.object({ x: z.number(), y: z.number() }).optional(),
18
+ out_handle: z.object({ x: z.number(), y: z.number() }).optional(),
19
+ })))
20
+ .optional()
21
+ .describe("Curve points with optional bezier handles"),
22
+ index: z.number().int().optional().describe("Insert position (add) or point index to remove"),
23
+ },
24
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
25
+ operationParam: "action",
26
+ },
27
+ ];
28
+ export function register(server, bridge, allowedTools) {
29
+ registerTools(server, bridge, pathTools, allowedTools);
30
+ }
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ import { callAndWrap } from "../registration/toolDispatch.js";
4
+ import { coercedBoolean } from "../shared/schemaCoercion.js";
5
+ import { isGroupLoaded } from "../groups/groupState.js";
6
+ export const playtestTools = [
7
+ {
8
+ name: "game_start",
9
+ method: "game.start",
10
+ description: "Start playtest. Blocks until runtime is ready by default; wait_for_runtime:false launches without blocking. scene_path:'main'|'current'(default)|res://path. if_running:'return' for idempotent mode.",
11
+ inputSchema: {
12
+ scene_path: z.string().optional(),
13
+ wait_for_runtime: coercedBoolean()
14
+ .default(true)
15
+ .describe("Defaults true — blocks until runtime connects (or times out) so runtime tools are immediately available. Pass wait_for_runtime:false to launch without blocking."),
16
+ runtime_poll: coercedBoolean()
17
+ .optional()
18
+ .describe("With if_running:'return', re-check whether the already-running game's runtime has since connected. Default false."),
19
+ if_running: z.enum(["return", "fail"]).optional(),
20
+ },
21
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
22
+ },
23
+ {
24
+ name: "game_stop",
25
+ method: "game.stop",
26
+ description: "Stop the currently-playing scene (idempotent — returns was_running:false if nothing was running). No params.",
27
+ inputSchema: {},
28
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
29
+ },
30
+ ];
31
+ export function register(server, bridge, allowedTools) {
32
+ const handlers = new Map();
33
+ handlers.set("game_start", async (input) => {
34
+ const result = await callAndWrap(bridge, "game.start", input);
35
+ // Inject hint if game started successfully and runtime group not loaded.
36
+ if (!isGroupLoaded("runtime") && result.content?.[0]?.type === "text") {
37
+ try {
38
+ const payload = JSON.parse(result.content[0].text);
39
+ if (payload.success && !result.isError) {
40
+ payload.group_hint =
41
+ "Game started. To interact with the running game, call " +
42
+ "discover_tools({request: 'runtime'}) to access runtime tools.";
43
+ result.content[0].text = JSON.stringify(payload);
44
+ }
45
+ }
46
+ catch {
47
+ /* parse failure — pass through unchanged */
48
+ }
49
+ }
50
+ // Nudge: the caller opted out of blocking (wait_for_runtime:false) but the
51
+ // runtime isn't ready yet — point them at the blocking and polling paths.
52
+ if (!input.wait_for_runtime && result.content?.[0]?.type === "text" && !result.isError) {
53
+ try {
54
+ const payload = JSON.parse(result.content[0].text);
55
+ if (payload.success && payload.runtime_ready === false) {
56
+ payload.hint =
57
+ "runtime_ready is false — runtime tools are not yet available. " +
58
+ "Call game_start with wait_for_runtime:true to block until ready, " +
59
+ "or poll with game_start(if_running:'return', runtime_poll:true).";
60
+ result.content[0].text = JSON.stringify(payload);
61
+ }
62
+ }
63
+ catch {
64
+ /* parse failure — pass through unchanged */
65
+ }
66
+ }
67
+ // Server-side wait-for-runtime: absorb the async gap so agents get
68
+ // single-call game launch instead of a two-step dance.
69
+ if (input.wait_for_runtime && result.content?.[0]?.type === "text" && !result.isError) {
70
+ try {
71
+ const payload = JSON.parse(result.content[0].text);
72
+ if (payload.runtime_discovery === "bridge" && bridge.waitForRuntimeConnection) {
73
+ const runtime = await bridge.waitForRuntimeConnection(10_000);
74
+ if (runtime) {
75
+ // Success — merge runtime info, drop the bridge-discovery marker.
76
+ payload.runtime_ready = true;
77
+ payload.runtime_port = runtime.port;
78
+ delete payload.runtime_discovery;
79
+ delete payload.hint;
80
+ }
81
+ else {
82
+ // Timeout — add fallback hint (toolkit hint is suppressed when
83
+ // wait_for_runtime=true, so the agent needs guidance here).
84
+ payload.hint =
85
+ "Game launched but runtime did not connect within 10s. " +
86
+ "Follow up with game_start(if_running:'return', runtime_poll:true) " +
87
+ "to retry, or check editor_get_console for startup errors.";
88
+ }
89
+ result.content[0].text = JSON.stringify(payload);
90
+ }
91
+ }
92
+ catch {
93
+ /* parse failure — pass through unchanged */
94
+ }
95
+ }
96
+ return result;
97
+ });
98
+ registerTools(server, bridge, playtestTools, allowedTools, { handlers });
99
+ }
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ import { jsonCoerce } from "../shared/schemaCoercion.js";
4
+ import { PROJECT_FILE_PATH } from "../security/pathGuard.js";
5
+ const colorSchema = z.object({
6
+ r: z.number().optional(),
7
+ g: z.number().optional(),
8
+ b: z.number().optional(),
9
+ a: z.number().optional(),
10
+ });
11
+ export const proceduralTools = [
12
+ {
13
+ name: "procedural_edit_gradient",
14
+ method: "procedural.edit_gradient",
15
+ description: "Create/edit a Gradient resource (.tres). Set color stops with offsets, add/remove points. For particles, sky, and visual effects.",
16
+ inputSchema: {
17
+ file_path: z.string().describe("Path for the .tres file (e.g. 'res://materials/sky_gradient.tres')"),
18
+ action: z
19
+ .enum(["set", "add_point", "remove_point"])
20
+ .optional()
21
+ .describe("set=replace all, add_point=add one, remove_point=delete by index (default: set)"),
22
+ points: z
23
+ .preprocess(jsonCoerce, z.array(z.object({
24
+ offset: z.number().describe("Position 0.0-1.0"),
25
+ color: z.preprocess(jsonCoerce, colorSchema).describe("Color {r,g,b,a?}"),
26
+ })))
27
+ .optional()
28
+ .describe("Gradient color stops"),
29
+ index: z.number().int().optional().describe("Point index (for remove_point)"),
30
+ interpolation_mode: z.enum(["linear", "cubic", "constant"]).optional().describe("Interpolation between stops"),
31
+ },
32
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
33
+ operationParam: "action",
34
+ pathParams: [PROJECT_FILE_PATH],
35
+ },
36
+ {
37
+ name: "procedural_edit_curve",
38
+ method: "procedural.edit_curve",
39
+ description: "Create/edit a Curve resource (.tres). Set control points with tangents for easing, falloff, and value mapping.",
40
+ inputSchema: {
41
+ file_path: z.string().describe("Path for the .tres file"),
42
+ action: z
43
+ .enum(["set", "add_point", "remove_point", "clear"])
44
+ .optional()
45
+ .describe("set=replace all, add_point=add one, remove_point=delete, clear=remove all (default: set)"),
46
+ points: z
47
+ .preprocess(jsonCoerce, z.array(z.object({
48
+ position: z.object({ x: z.number(), y: z.number() }).describe("x=input 0-1, y=output value"),
49
+ left_tangent: z.number().optional(),
50
+ right_tangent: z.number().optional(),
51
+ left_mode: z.enum(["free", "linear"]).optional(),
52
+ right_mode: z.enum(["free", "linear"]).optional(),
53
+ })))
54
+ .optional()
55
+ .describe("Curve control points"),
56
+ index: z.number().int().optional().describe("Point index (for remove_point)"),
57
+ min_value: z.number().optional().describe("Curve minimum Y value"),
58
+ max_value: z.number().optional().describe("Curve maximum Y value"),
59
+ },
60
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
61
+ operationParam: "action",
62
+ pathParams: [PROJECT_FILE_PATH],
63
+ },
64
+ {
65
+ name: "procedural_edit_noise",
66
+ method: "procedural.edit_noise",
67
+ description: "Create/edit a FastNoiseLite resource (.tres). Configure noise type, fractal, cellular, and domain warp for procedural generation.",
68
+ inputSchema: {
69
+ file_path: z.string().describe("Path for the .tres file"),
70
+ noise_type: z
71
+ .enum(["simplex", "simplex_smooth", "cellular", "perlin", "value", "value_cubic"])
72
+ .optional()
73
+ .describe("Noise algorithm"),
74
+ seed: z.number().int().optional().describe("Random seed"),
75
+ frequency: z.number().optional().describe("Base frequency (default 0.01)"),
76
+ octaves: z.number().int().optional().describe("Fractal octaves 1-10"),
77
+ lacunarity: z.number().optional().describe("Octave frequency multiplier"),
78
+ gain: z.number().optional().describe("Octave amplitude multiplier"),
79
+ fractal_type: z.enum(["none", "fbm", "ridged", "ping_pong"]).optional().describe("Fractal type"),
80
+ cellular_distance_function: z.enum(["euclidean", "euclidean_squared", "manhattan", "hybrid"]).optional(),
81
+ cellular_return_type: z
82
+ .enum([
83
+ "cell_value",
84
+ "distance",
85
+ "distance2",
86
+ "distance2_add",
87
+ "distance2_sub",
88
+ "distance2_mul",
89
+ "distance2_div",
90
+ ])
91
+ .optional(),
92
+ domain_warp_enabled: z.boolean().optional(),
93
+ domain_warp_amplitude: z.number().optional(),
94
+ },
95
+ annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
96
+ pathParams: [PROJECT_FILE_PATH],
97
+ },
98
+ ];
99
+ export function register(server, bridge, allowedTools) {
100
+ registerTools(server, bridge, proceduralTools, allowedTools);
101
+ }
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { registerTools } from "../registration/toolRegistry.js";
3
+ import { PROJECT_FILE_PATH } from "../security/pathGuard.js";
4
+ export const resourceTools = [
5
+ {
6
+ name: "resource_load",
7
+ method: "resource.load",
8
+ description: "Load a res:// resource and return { class, path, properties, metadata }. Heavy fields (image, mesh_arrays) pruned; Texture2D gets size in metadata.",
9
+ inputSchema: { file_path: z.string() },
10
+ annotations: { readOnlyHint: true, openWorldHint: false },
11
+ pathParams: [PROJECT_FILE_PATH],
12
+ },
13
+ {
14
+ name: "resource_write",
15
+ method: "resource.write",
16
+ description: "Write/create a .tres/.res resource. If file exists, updates properties. If not, 'type' (class name) is required to create it. " +
17
+ "For TileSets, use tileset_create instead (handles atlas + physics setup).",
18
+ inputSchema: {
19
+ file_path: z.string(),
20
+ properties: z
21
+ .record(z.string(), z.unknown())
22
+ .optional()
23
+ .describe("Property values. Sub-resources in dicts: use {type:'NewResource', class:'ClassName', properties:{...}}."),
24
+ type: z.string().optional().describe("Resource class name. Required when creating a new resource."),
25
+ },
26
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, destructiveHint: false },
27
+ successHint: "Verify with resource_load. Assign to node via node_set_property with Resource type tag.",
28
+ pathParams: [PROJECT_FILE_PATH],
29
+ },
30
+ {
31
+ name: "resource_delete",
32
+ method: "resource.delete",
33
+ description: "Delete the .tres/.res and its .uid companion at file_path. No active-use guard (Godot refs survive file deletion; detect orphans via editor_get_console).",
34
+ inputSchema: { file_path: z.string() },
35
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
36
+ successHint: "For scenes use scene_delete. For scripts use script_delete. Non-resource files: file_delete. " +
37
+ "On Windows/OneDrive a transient lock can leave the .tres on disk despite deindexed:true — retry the delete to clear it.",
38
+ pathParams: [PROJECT_FILE_PATH],
39
+ },
40
+ ];
41
+ // resource_load `properties` is already <untrusted>-wrapped at origin by the
42
+ // toolkit (resource_commands.gd wraps the whole JSON.stringify(properties) in
43
+ // one envelope). Do NOT re-wrap here: the wrapper scrubs inner envelope tags,
44
+ // so double-wrapping corrupts the envelope.
45
+ export function register(server, bridge, allowedTools) {
46
+ registerTools(server, bridge, resourceTools, allowedTools);
47
+ }