Skip to content

Commit 26f10d1

Browse files
authored
[Audit v2 #364] Extract resolve-or-error helper; migrate 38+ duplicate sites (#389)
1 parent f691dd8 commit 26f10d1

22 files changed

Lines changed: 335 additions & 202 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@tool
2+
class_name McpNodeValidator
3+
extends RefCounted
4+
5+
## Shared resolve-or-error helper that subsumes the 38+ sites where
6+
## handlers each rolled their own "is the editor ready, does the path
7+
## resolve, otherwise return EDITOR_NOT_READY / NODE_NOT_FOUND" guard.
8+
##
9+
## audit-v2 #20 (issue #364). Uses the audit-v2 #21 (issue #365) error
10+
## vocabulary.
11+
12+
const McpScenePath = preload("res://addons/godot_ai/utils/scene_path.gd")
13+
const McpErrorCodes = preload("res://addons/godot_ai/utils/error_codes.gd")
14+
15+
16+
## Resolve a scene-relative path to the live Node, or return a structured
17+
## error dict.
18+
##
19+
## Success shape: `{"node": Node, "scene_root": Node, "path": String}`.
20+
## Error shape: matches `McpErrorCodes.make(...)` so callers can
21+
## `return resolved` to propagate.
22+
##
23+
## Errors (in order checked):
24+
## - `MISSING_REQUIRED_PARAM`: `node_path` is empty
25+
## - `EDITOR_NOT_READY`: no scene open
26+
## - `EDITED_SCENE_MISMATCH`: caller pinned `scene_file` and the open
27+
## scene's path doesn't match
28+
## - `NODE_NOT_FOUND`: `node_path` doesn't resolve under the scene root
29+
##
30+
## `param_name` is the agent-facing name reported in the
31+
## `MISSING_REQUIRED_PARAM` message — handlers pass "node_path",
32+
## "player_path", "target_path", etc. so the error reads like the
33+
## hand-written messages it replaces.
34+
static func resolve_or_error(
35+
node_path: String,
36+
param_name: String = "path",
37+
scene_file: String = "",
38+
) -> Dictionary:
39+
if node_path.is_empty():
40+
return McpErrorCodes.make(
41+
McpErrorCodes.MISSING_REQUIRED_PARAM,
42+
"Missing required param: %s" % param_name,
43+
)
44+
var scene_check := McpScenePath.require_edited_scene(scene_file)
45+
if scene_check.has("error"):
46+
return scene_check
47+
var scene_root: Node = scene_check.node
48+
var node := McpScenePath.resolve(node_path, scene_root)
49+
if node == null:
50+
return McpErrorCodes.make(
51+
McpErrorCodes.NODE_NOT_FOUND,
52+
McpScenePath.format_node_error(node_path, scene_root),
53+
)
54+
return {"node": node, "scene_root": scene_root, "path": node_path}
55+
56+
57+
## When the caller needs the scene root but no specific node yet — e.g.
58+
## handlers that walk children or filter by group. Returns either
59+
## `{"scene_root": Node}` or a `McpErrorCodes.make(...)` error dict.
60+
static func require_scene_or_error(scene_file: String = "") -> Dictionary:
61+
var scene_check := McpScenePath.require_edited_scene(scene_file)
62+
if scene_check.has("error"):
63+
return scene_check
64+
return {"scene_root": scene_check.node}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://dn75jifad0ghx

plugin/addons/godot_ai/handlers/animation_handler.gd

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ func create_player(params: Dictionary) -> Dictionary:
5050
var parent_path: String = params.get("parent_path", "")
5151
var node_name: String = params.get("name", "AnimationPlayer")
5252

53-
var scene_root := EditorInterface.get_edited_scene_root()
54-
if scene_root == null:
55-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
53+
var _scene_check := McpNodeValidator.require_scene_or_error()
54+
if _scene_check.has("error"):
55+
return _scene_check
56+
var scene_root: Node = _scene_check.scene_root
5657

5758
var parent: Node = scene_root
5859
if not parent_path.is_empty():
@@ -726,9 +727,10 @@ func _create_scene_pinned_action(action_label: String) -> void:
726727
## If the resolved node exists but isn't an AnimationPlayer, that's still an
727728
## error — we don't clobber an existing node of a different type.
728729
func _resolve_player(player_path: String, create_if_missing: bool = false) -> Dictionary:
729-
var scene_root := EditorInterface.get_edited_scene_root()
730-
if scene_root == null:
731-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
730+
var _scene_check := McpNodeValidator.require_scene_or_error()
731+
if _scene_check.has("error"):
732+
return _scene_check
733+
var scene_root: Node = _scene_check.scene_root
732734
var node := McpScenePath.resolve(player_path, scene_root)
733735
if node == null:
734736
if not create_if_missing:
@@ -790,9 +792,10 @@ func _instantiate_player(player_path: String, scene_root: Node) -> Dictionary:
790792
## staged. If the node exists but isn't an AnimationPlayer, errors exactly
791793
## like `_resolve_player` — that's a genuine type mismatch, not a missing node.
792794
func _resolve_or_create_player(player_path: String) -> Dictionary:
793-
var scene_root := EditorInterface.get_edited_scene_root()
794-
if scene_root == null:
795-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
795+
var _scene_check := McpNodeValidator.require_scene_or_error()
796+
if _scene_check.has("error"):
797+
return _scene_check
798+
var scene_root: Node = _scene_check.scene_root
796799
if McpScenePath.resolve(player_path, scene_root) != null:
797800
# Node exists — delegate so the type-mismatch error stays identical
798801
# to _resolve_player's.
@@ -829,12 +832,10 @@ func _resolve_or_create_player(player_path: String) -> Dictionary:
829832

830833
## Resolve for read operations (no library requirement).
831834
func _resolve_player_read(player_path: String) -> Dictionary:
832-
var scene_root := EditorInterface.get_edited_scene_root()
833-
if scene_root == null:
834-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
835-
var node := McpScenePath.resolve(player_path, scene_root)
836-
if node == null:
837-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(player_path, scene_root))
835+
var resolved := McpNodeValidator.resolve_or_error(player_path, "player_path")
836+
if resolved.has("error"):
837+
return resolved
838+
var node: Node = resolved.node
838839
if not node is AnimationPlayer:
839840
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE,
840841
"Node at %s is not an AnimationPlayer (got %s)" % [player_path, node.get_class()])

plugin/addons/godot_ai/handlers/audio_handler.gd

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ func create_player(params: Dictionary) -> Dictionary:
5050
"Invalid audio player type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
5151
)
5252

53-
var scene_root := EditorInterface.get_edited_scene_root()
54-
if scene_root == null:
55-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
53+
var _scene_check := McpNodeValidator.require_scene_or_error()
54+
if _scene_check.has("error"):
55+
return _scene_check
56+
var scene_root: Node = _scene_check.scene_root
5657

5758
var parent: Node = scene_root
5859
if not parent_path.is_empty():
@@ -319,12 +320,10 @@ static func _instantiate_player(type_str: String) -> Node:
319320

320321

321322
func _resolve_player(player_path: String) -> Dictionary:
322-
var scene_root := EditorInterface.get_edited_scene_root()
323-
if scene_root == null:
324-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
325-
var node := McpScenePath.resolve(player_path, scene_root)
326-
if node == null:
327-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(player_path, scene_root))
323+
var resolved := McpNodeValidator.resolve_or_error(player_path, "player_path")
324+
if resolved.has("error"):
325+
return resolved
326+
var node: Node = resolved.node
328327
var is_player := node is AudioStreamPlayer \
329328
or node is AudioStreamPlayer2D \
330329
or node is AudioStreamPlayer3D

plugin/addons/godot_ai/handlers/camera_handler.gd

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,10 @@ func create_camera(params: Dictionary) -> Dictionary:
477477
"Invalid camera type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
478478
)
479479

480-
var scene_root := EditorInterface.get_edited_scene_root()
481-
if scene_root == null:
482-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
480+
var _scene_check := McpNodeValidator.require_scene_or_error()
481+
if _scene_check.has("error"):
482+
return _scene_check
483+
var scene_root: Node = _scene_check.scene_root
483484

484485
var parent: Node = scene_root
485486
if not parent_path.is_empty():
@@ -869,9 +870,10 @@ func follow_2d(params: Dictionary) -> Dictionary:
869870
# ============================================================================
870871

871872
func get_camera(params: Dictionary) -> Dictionary:
872-
var scene_root := EditorInterface.get_edited_scene_root()
873-
if scene_root == null:
874-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
873+
var _scene_check := McpNodeValidator.require_scene_or_error()
874+
if _scene_check.has("error"):
875+
return _scene_check
876+
var scene_root: Node = _scene_check.scene_root
875877

876878
var camera_path: String = params.get("camera_path", "")
877879
var node: Node = null
@@ -952,9 +954,10 @@ func get_camera(params: Dictionary) -> Dictionary:
952954
# ============================================================================
953955

954956
func list_cameras(_params: Dictionary) -> Dictionary:
955-
var scene_root := EditorInterface.get_edited_scene_root()
956-
if scene_root == null:
957-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
957+
var _scene_check := McpNodeValidator.require_scene_or_error()
958+
if _scene_check.has("error"):
959+
return _scene_check
960+
var scene_root: Node = _scene_check.scene_root
958961

959962
var cams := _list_cameras_in_scene(scene_root, "")
960963
var out: Array[Dictionary] = []
@@ -999,9 +1002,10 @@ func apply_preset(params: Dictionary) -> Dictionary:
9991002
"Invalid camera type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
10001003
)
10011004

1002-
var scene_root := EditorInterface.get_edited_scene_root()
1003-
if scene_root == null:
1004-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
1005+
var _scene_check := McpNodeValidator.require_scene_or_error()
1006+
if _scene_check.has("error"):
1007+
return _scene_check
1008+
var scene_root: Node = _scene_check.scene_root
10051009

10061010
var parent: Node = scene_root
10071011
if not parent_path.is_empty():
@@ -1088,15 +1092,14 @@ static func _camera_type_str(node: Node) -> String:
10881092

10891093

10901094
func _resolve_camera(params: Dictionary) -> Dictionary:
1091-
var node_path: String = params.get("camera_path", "")
1092-
if node_path.is_empty():
1093-
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: camera_path")
1094-
var scene_root := EditorInterface.get_edited_scene_root()
1095-
if scene_root == null:
1096-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
1097-
var node := McpScenePath.resolve(node_path, scene_root)
1098-
if node == null:
1099-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
1095+
var resolved := McpNodeValidator.resolve_or_error(
1096+
params.get("camera_path", ""), "camera_path",
1097+
)
1098+
if resolved.has("error"):
1099+
return resolved
1100+
var node: Node = resolved.node
1101+
var node_path: String = resolved.path
1102+
var scene_root: Node = resolved.scene_root
11001103
if not _is_camera(node):
11011104
return McpErrorCodes.make(
11021105
McpErrorCodes.WRONG_TYPE,

plugin/addons/godot_ai/handlers/control_draw_recipe_handler.gd

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@ func control_draw_recipe(params: Dictionary) -> Dictionary:
2727
if typeof(ops_raw) != TYPE_ARRAY:
2828
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "ops must be an Array")
2929

30-
var scene_root := EditorInterface.get_edited_scene_root()
31-
if scene_root == null:
32-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
33-
34-
var node := McpScenePath.resolve(path, scene_root)
35-
if node == null:
36-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(path, scene_root))
30+
var _resolved := McpNodeValidator.resolve_or_error(path, "path")
31+
if _resolved.has("error"):
32+
return _resolved
33+
var node: Node = _resolved.node
34+
var scene_root: Node = _resolved.scene_root
3735
if not node is Control:
3836
return McpErrorCodes.make(
3937
McpErrorCodes.WRONG_TYPE,

plugin/addons/godot_ai/handlers/curve_handler.gd

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ func set_points(params: Dictionary) -> Dictionary:
5454
)
5555
curve = loaded_curve.duplicate()
5656
else:
57-
var scene_root := EditorInterface.get_edited_scene_root()
58-
if scene_root == null:
59-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
57+
var _scene_check := McpNodeValidator.require_scene_or_error()
58+
if _scene_check.has("error"):
59+
return _scene_check
60+
var scene_root: Node = _scene_check.scene_root
6061
node = McpScenePath.resolve(node_path, scene_root)
6162
if node == null:
6263
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))

plugin/addons/godot_ai/handlers/editor_handler.gd

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,10 @@ func take_screenshot(params: Dictionary) -> Dictionary:
300300
## Handle view_target: temporarily reposition the editor's own camera to
301301
## frame one or more target nodes, force a render, capture, then restore.
302302
if not view_target.is_empty() and source == "viewport":
303-
var scene_root := EditorInterface.get_edited_scene_root()
304-
if scene_root == null:
305-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
303+
var _scene_check := McpNodeValidator.require_scene_or_error()
304+
if _scene_check.has("error"):
305+
return _scene_check
306+
var scene_root: Node = _scene_check.scene_root
306307

307308
## Parse comma-separated paths, deduplicate
308309
var raw_paths := view_target.split(",")
@@ -451,9 +452,10 @@ func take_screenshot(params: Dictionary) -> Dictionary:
451452
## throwaway SubViewport, so the output has no editor gizmos, selection
452453
## outlines, or grid lines.
453454
func _take_cinematic_screenshot(max_resolution: int) -> Dictionary:
454-
var scene_root := EditorInterface.get_edited_scene_root()
455-
if scene_root == null:
456-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
455+
var _scene_check := McpNodeValidator.require_scene_or_error()
456+
if _scene_check.has("error"):
457+
return _scene_check
458+
var scene_root: Node = _scene_check.scene_root
457459

458460
var scene_camera := _find_current_camera_3d(scene_root)
459461
if scene_camera == null:

plugin/addons/godot_ai/handlers/environment_handler.gd

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,11 @@ static func _apply_preset(env: Environment, sky_material: ProceduralSkyMaterial,
139139

140140

141141
func _assign_environment(env: Environment, sky: Sky, sky_material: ProceduralSkyMaterial, node_path: String, preset: String) -> Dictionary:
142-
var scene_root := EditorInterface.get_edited_scene_root()
143-
if scene_root == null:
144-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
145-
146-
var node := McpScenePath.resolve(node_path, scene_root)
147-
if node == null:
148-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
142+
var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
143+
if _resolved.has("error"):
144+
return _resolved
145+
var node: Node = _resolved.node
146+
var scene_root: Node = _resolved.scene_root
149147
if not (node is WorldEnvironment):
150148
return McpErrorCodes.make(
151149
McpErrorCodes.WRONG_TYPE,

plugin/addons/godot_ai/handlers/material_handler.gd

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,11 @@ func assign_material(params: Dictionary) -> Dictionary:
344344
if node_path.is_empty():
345345
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: node_path")
346346

347-
var scene_root := EditorInterface.get_edited_scene_root()
348-
if scene_root == null:
349-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
350-
351-
var node := McpScenePath.resolve(node_path, scene_root)
352-
if node == null:
353-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
347+
var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
348+
if _resolved.has("error"):
349+
return _resolved
350+
var node: Node = _resolved.node
351+
var scene_root: Node = _resolved.scene_root
354352

355353
var slot: String = params.get("slot", "override")
356354
var resource_path: String = params.get("resource_path", "")
@@ -438,13 +436,11 @@ func apply_to_node(params: Dictionary) -> Dictionary:
438436
"Invalid material type '%s'. Valid: %s" % [type_str, ", ".join(_TYPE_TO_CLASS.keys())]
439437
)
440438

441-
var scene_root := EditorInterface.get_edited_scene_root()
442-
if scene_root == null:
443-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
444-
445-
var node := McpScenePath.resolve(node_path, scene_root)
446-
if node == null:
447-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
439+
var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
440+
if _resolved.has("error"):
441+
return _resolved
442+
var node: Node = _resolved.node
443+
var scene_root: Node = _resolved.scene_root
448444

449445
var slot: String = params.get("slot", "override")
450446
var slot_result := _resolve_slot_property(node, slot)
@@ -586,12 +582,11 @@ func apply_preset(params: Dictionary) -> Dictionary:
586582

587583
var assigned := false
588584
if not node_path.is_empty():
589-
var scene_root := EditorInterface.get_edited_scene_root()
590-
if scene_root == null:
591-
return McpErrorCodes.make(McpErrorCodes.EDITOR_NOT_READY, "No scene open")
592-
var node := McpScenePath.resolve(node_path, scene_root)
593-
if node == null:
594-
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
585+
var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
586+
if _resolved.has("error"):
587+
return _resolved
588+
var node: Node = _resolved.node
589+
var scene_root: Node = _resolved.scene_root
595590
var slot_result := _resolve_slot_property(node, params.get("slot", "override"))
596591
if slot_result.has("error"):
597592
return slot_result

0 commit comments

Comments
 (0)