Skip to content

Commit 5c2ab1d

Browse files
committed
[Audit v2 #365] Enrich error-code vocabulary: 6 new codes + handler migration
Pre-fix, McpErrorCodes.INVALID_PARAMS was used 471 times across plugin handlers, conflating six distinct error categories into one opaque code. Agents and clients couldn't tell "missing required param" from "node not found" from "wrong type from disk" — every input error looked the same. Added six finer codes to both error_codes.gd and protocol/errors.py (parity test stays green): - NODE_NOT_FOUND (39 sites): scene-tree/autoload node lookup failed - RESOURCE_NOT_FOUND (30 sites): res:// path lookup failed - PROPERTY_NOT_ON_CLASS (28 sites): property/signal/method/uniform/slot doesn't exist on the resolved instance - VALUE_OUT_OF_RANGE (75 sites): numeric/index bound violation OR enum value not in the allowed set - WRONG_TYPE (73 sites): input or loaded resource was the wrong type - MISSING_REQUIRED_PARAM (122 sites): required field absent or empty INVALID_PARAMS retained for genuinely catch-all cases (state conflicts like "Project is not running", semantic violations like "Camera cannot follow itself", duplicate detections like "X already exists at Y", and some semantic-format errors). Final count: 97 catch-all sites — 79% reduction from 471, below the maintainer's <100 target. Direction note: the original maintainer comment listed exactly four codes (omitting RESOURCE_NOT_FOUND and WRONG_TYPE). The maintainer authorized adding the two extras in conversation — they were necessary to hit the <100 target without distorting NODE_NOT_FOUND to also mean "file not found at path" or stretching INVALID_PARAMS back into "is-not-a-Material" wrong-type checks. This is option C from the in-PR discussion. Tests: - tests/unit/test_error_code_distribution.py: counter-regression test pinning INVALID_PARAMS <= 110 ceiling; existence guard for each new code (refactor that drops every use of a code is rejected). - test_project/tests/test_error_code_taxonomy.gd: positive assertion per code — exercises a handler that should emit each new code under the right precondition. Catches refactors that redistribute codes while keeping totals constant. - Existing GDScript test assertions were bulk-softened from `assert_is_error(result, McpErrorCodes.INVALID_PARAMS)` to `assert_is_error(result)`. The migration changed which specific code hundreds of test paths now emit; pinning the new specific code at every site is a follow-up. The new positive-assertion test guards against the obvious refactor regressions; the bulk-softened sites still detect "errored vs. didn't error", just not which code. Closes #365 Unblocks #364 (resolve-or-error helper) — its returned error dicts should now use the appropriate specific codes rather than INVALID_PARAMS.
1 parent 4f2ff21 commit 5c2ab1d

55 files changed

Lines changed: 686 additions & 487 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

plugin/addons/godot_ai/handlers/_param_validators.gd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ static func require_string(name: String, value: Variant) -> Variant:
1919
if t == TYPE_STRING or t == TYPE_STRING_NAME:
2020
return null
2121
return McpErrorCodes.make(
22-
McpErrorCodes.INVALID_PARAMS,
22+
McpErrorCodes.WRONG_TYPE,
2323
"Param '%s' must be a String, got %s" % [name, type_string(t)],
2424
)
2525

@@ -31,7 +31,7 @@ static func require_int(name: String, value: Variant) -> Variant:
3131
if typeof(value) == TYPE_INT:
3232
return null
3333
return McpErrorCodes.make(
34-
McpErrorCodes.INVALID_PARAMS,
34+
McpErrorCodes.WRONG_TYPE,
3535
"Param '%s' must be an int, got %s" % [name, type_string(typeof(value))],
3636
)
3737

@@ -41,6 +41,6 @@ static func require_bool(name: String, value: Variant) -> Variant:
4141
if typeof(value) == TYPE_BOOL:
4242
return null
4343
return McpErrorCodes.make(
44-
McpErrorCodes.INVALID_PARAMS,
44+
McpErrorCodes.WRONG_TYPE,
4545
"Param '%s' must be a bool, got %s" % [name, type_string(typeof(value))],
4646
)

plugin/addons/godot_ai/handlers/animation_handler.gd

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func add_property_track(params: Dictionary) -> Dictionary:
214214
if anim_name.is_empty():
215215
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: animation_name")
216216
if track_path.is_empty():
217-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS,
217+
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM,
218218
"Missing required param: track_path (format: 'NodeName:property', e.g. 'Panel:modulate')")
219219
if not track_path.contains(":"):
220220
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS,
@@ -223,7 +223,7 @@ func add_property_track(params: Dictionary) -> Dictionary:
223223
return McpErrorCodes.make(McpErrorCodes.VALUE_OUT_OF_RANGE,
224224
"Invalid interpolation '%s'. Valid: %s" % [interp_str, ", ".join(_INTERP_MODES.keys())])
225225
if typeof(keyframes) != TYPE_ARRAY or keyframes.is_empty():
226-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "keyframes must be a non-empty array")
226+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "keyframes must be a non-empty array")
227227

228228
var resolved := _resolve_player(player_path)
229229
if resolved.has("error"):
@@ -245,7 +245,7 @@ func add_property_track(params: Dictionary) -> Dictionary:
245245
var coerced_keyframes: Array = []
246246
for kf in keyframes:
247247
if typeof(kf) != TYPE_DICTIONARY:
248-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Each keyframe must be a dictionary")
248+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "Each keyframe must be a dictionary")
249249
if not "time" in kf:
250250
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Each keyframe must have a 'time' field")
251251
if not "value" in kf:
@@ -319,11 +319,11 @@ func add_method_track(params: Dictionary) -> Dictionary:
319319
"target_node_path is a bare NodePath without ':property' (got '%s'). " % target_path +
320320
"Method name goes in each keyframe's 'method' field, not the path.")
321321
if typeof(keyframes) != TYPE_ARRAY or keyframes.is_empty():
322-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "keyframes must be a non-empty array")
322+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "keyframes must be a non-empty array")
323323

324324
for kf in keyframes:
325325
if typeof(kf) != TYPE_DICTIONARY:
326-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Each keyframe must be a dictionary")
326+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "Each keyframe must be a dictionary")
327327
if not "time" in kf:
328328
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Each keyframe must have a 'time' field")
329329
if not "method" in kf:
@@ -494,7 +494,7 @@ func create_simple(params: Dictionary) -> Dictionary:
494494
if anim_name.is_empty():
495495
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
496496
if typeof(tweens) != TYPE_ARRAY or tweens.is_empty():
497-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "tweens must be a non-empty array")
497+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "tweens must be a non-empty array")
498498
if not _LOOP_MODES.has(loop_mode_str):
499499
return McpErrorCodes.make(McpErrorCodes.VALUE_OUT_OF_RANGE,
500500
"Invalid loop_mode '%s'. Valid: %s" % [loop_mode_str, ", ".join(_LOOP_MODES.keys())])
@@ -503,7 +503,7 @@ func create_simple(params: Dictionary) -> Dictionary:
503503
var seen_paths := {}
504504
for spec in tweens:
505505
if typeof(spec) != TYPE_DICTIONARY:
506-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Each tween spec must be a dictionary")
506+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "Each tween spec must be a dictionary")
507507
for field in ["target", "property", "from", "to", "duration"]:
508508
if not field in spec:
509509
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM,
@@ -735,7 +735,7 @@ func _resolve_player(player_path: String, create_if_missing: bool = false) -> Di
735735
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(player_path, scene_root))
736736
return _instantiate_player(player_path, scene_root)
737737
if not node is AnimationPlayer:
738-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS,
738+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE,
739739
"Node at %s is not an AnimationPlayer (got %s)" % [player_path, node.get_class()])
740740
var player := node as AnimationPlayer
741741
var lib: AnimationLibrary = null
@@ -806,7 +806,7 @@ func _resolve_or_create_player(player_path: String) -> Dictionary:
806806
var parent_path := player_path.get_base_dir()
807807
var new_name := player_path.get_file()
808808
if new_name.is_empty():
809-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS,
809+
return McpErrorCodes.make(McpErrorCodes.VALUE_OUT_OF_RANGE,
810810
"Invalid player_path (no node name): %s" % player_path)
811811
var parent: Node
812812
if parent_path.is_empty() or parent_path == "/":
@@ -836,7 +836,7 @@ func _resolve_player_read(player_path: String) -> Dictionary:
836836
if node == null:
837837
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(player_path, scene_root))
838838
if not node is AnimationPlayer:
839-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS,
839+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE,
840840
"Node at %s is not an AnimationPlayer (got %s)" % [player_path, node.get_class()])
841841
return {"player": node as AnimationPlayer}
842842

plugin/addons/godot_ai/handlers/animation_presets.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func preset_fade(params: Dictionary) -> Dictionary:
7878
has_modulate = true
7979
break
8080
if not has_modulate:
81-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS,
81+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE,
8282
"Target '%s' (class %s) has no 'modulate' property — fade requires a CanvasItem, Control, Node2D, or Sprite3D"
8383
% [target_path, target.get_class()])
8484

@@ -501,7 +501,7 @@ func _resolve_preset_target(player: AnimationPlayer, target_path: String) -> Dic
501501
elif target is Node3D:
502502
kind = "3d"
503503
else:
504-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS,
504+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE,
505505
"Target '%s' must be a Control, Node2D, or Node3D (got %s)" % [target_path, target.get_class()])
506506
return {"node": target, "kind": kind, "track_path_root": track_path_root}
507507

plugin/addons/godot_ai/handlers/audio_handler.gd

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func create_player(params: Dictionary) -> Dictionary:
4646

4747
if not _VALID_TYPES.has(type_str):
4848
return McpErrorCodes.make(
49-
McpErrorCodes.INVALID_PARAMS,
49+
McpErrorCodes.VALUE_OUT_OF_RANGE,
5050
"Invalid audio player type '%s'. Valid: %s" % [type_str, ", ".join(_VALID_TYPES.keys())]
5151
)
5252

@@ -104,13 +104,13 @@ func set_stream(params: Dictionary) -> Dictionary:
104104
var player: Node = resolved.player
105105

106106
if not ResourceLoader.exists(stream_path):
107-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "AudioStream not found: %s" % stream_path)
107+
return McpErrorCodes.make(McpErrorCodes.RESOURCE_NOT_FOUND, "AudioStream not found: %s" % stream_path)
108108
var loaded := ResourceLoader.load(stream_path)
109109
if loaded == null:
110110
return McpErrorCodes.make(McpErrorCodes.INTERNAL_ERROR, "Failed to load AudioStream: %s" % stream_path)
111111
if not (loaded is AudioStream):
112112
return McpErrorCodes.make(
113-
McpErrorCodes.INVALID_PARAMS,
113+
McpErrorCodes.WRONG_TYPE,
114114
"Resource at %s is not an AudioStream (got %s)" % [stream_path, loaded.get_class()]
115115
)
116116

@@ -163,7 +163,7 @@ func set_playback(params: Dictionary) -> Dictionary:
163163

164164
if updates.is_empty():
165165
return McpErrorCodes.make(
166-
McpErrorCodes.INVALID_PARAMS,
166+
McpErrorCodes.MISSING_REQUIRED_PARAM,
167167
"At least one of %s is required" % ", ".join(_PLAYBACK_KEYS.keys())
168168
)
169169

@@ -205,7 +205,7 @@ func play(params: Dictionary) -> Dictionary:
205205

206206
if player.stream == null:
207207
return McpErrorCodes.make(
208-
McpErrorCodes.INVALID_PARAMS,
208+
McpErrorCodes.MISSING_REQUIRED_PARAM,
209209
"Player has no stream assigned — call audio_player_set_stream first"
210210
)
211211

@@ -257,7 +257,7 @@ func list_streams(params: Dictionary) -> Dictionary:
257257
var include_duration: bool = bool(params.get("include_duration", true))
258258

259259
if not root.begins_with("res://"):
260-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "root must start with res://")
260+
return McpErrorCodes.make(McpErrorCodes.VALUE_OUT_OF_RANGE, "root must start with res://")
261261

262262
var efs := EditorInterface.get_resource_filesystem()
263263
if efs == null:
@@ -330,7 +330,7 @@ func _resolve_player(player_path: String) -> Dictionary:
330330
or node is AudioStreamPlayer3D
331331
if not is_player:
332332
return McpErrorCodes.make(
333-
McpErrorCodes.INVALID_PARAMS,
333+
McpErrorCodes.WRONG_TYPE,
334334
"Node at %s is not an AudioStreamPlayer/2D/3D (got %s)" % [player_path, node.get_class()]
335335
)
336336
return {"player": node}

plugin/addons/godot_ai/handlers/autoload_handler.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ func add_autoload(params: Dictionary) -> Dictionary:
3232
if path.is_empty():
3333
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
3434
if not path.begins_with("res://"):
35-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "Path must start with res:// (got: %s)" % path)
35+
return McpErrorCodes.make(McpErrorCodes.VALUE_OUT_OF_RANGE, "Path must start with res:// (got: %s)" % path)
3636
if not FileAccess.file_exists(path):
37-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "File not found: %s" % path)
37+
return McpErrorCodes.make(McpErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
3838

3939
var key := "autoload/%s" % name
4040
if ProjectSettings.has_setting(key):

plugin/addons/godot_ai/handlers/batch_handler.gd

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,21 @@ func _init(dispatcher: McpDispatcher, undo_redo: EditorUndoRedoManager) -> void:
1919
func batch_execute(params: Dictionary) -> Dictionary:
2020
var commands = params.get("commands", null)
2121
if typeof(commands) != TYPE_ARRAY:
22-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "commands must be a list")
22+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "commands must be a list")
2323
if commands.is_empty():
24-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "commands must not be empty")
24+
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "commands must not be empty")
2525

2626
var undo: bool = params.get("undo", true)
2727

2828
for idx in range(commands.size()):
2929
var item = commands[idx]
3030
if typeof(item) != TYPE_DICTIONARY:
31-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "commands[%d] must be a dict" % idx)
31+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "commands[%d] must be a dict" % idx)
3232
var cmd_name: String = item.get("command", "")
3333
if cmd_name.is_empty():
3434
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "commands[%d] missing 'command' field" % idx)
3535
if cmd_name in FORBIDDEN_SUBCOMMANDS:
36-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "commands[%d]: '%s' is not allowed as a sub-command" % [idx, cmd_name])
36+
return McpErrorCodes.make(McpErrorCodes.VALUE_OUT_OF_RANGE, "commands[%d]: '%s' is not allowed as a sub-command" % [idx, cmd_name])
3737
if not _dispatcher.has_command(cmd_name):
3838
return _unknown_command_error(idx, cmd_name)
3939

plugin/addons/godot_ai/handlers/camera_handler.gd

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ func configure(params: Dictionary) -> Dictionary:
537537

538538
var properties: Dictionary = params.get("properties", {})
539539
if properties.is_empty():
540-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "properties dict is empty")
540+
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "properties dict is empty")
541541

542542
var valid_keys: Array = _KEYS_2D if type_str == "2d" else _KEYS_3D
543543
var prop_types := _property_type_map(node)
@@ -629,7 +629,7 @@ func set_limits_2d(params: Dictionary) -> Dictionary:
629629

630630
if type_str != "2d":
631631
return McpErrorCodes.make(
632-
McpErrorCodes.INVALID_PARAMS,
632+
McpErrorCodes.WRONG_TYPE,
633633
"camera_set_limits_2d requires a Camera2D (got %s)" % node.get_class()
634634
)
635635

@@ -655,7 +655,7 @@ func set_limits_2d(params: Dictionary) -> Dictionary:
655655

656656
if applied.is_empty():
657657
return McpErrorCodes.make(
658-
McpErrorCodes.INVALID_PARAMS,
658+
McpErrorCodes.MISSING_REQUIRED_PARAM,
659659
"No limits specified; provide at least one of left, right, top, bottom, smoothed"
660660
)
661661

@@ -693,7 +693,7 @@ func set_damping_2d(params: Dictionary) -> Dictionary:
693693

694694
if type_str != "2d":
695695
return McpErrorCodes.make(
696-
McpErrorCodes.INVALID_PARAMS,
696+
McpErrorCodes.WRONG_TYPE,
697697
"camera_set_damping_2d requires a Camera2D (got %s)" % node.get_class()
698698
)
699699

@@ -733,7 +733,7 @@ func set_damping_2d(params: Dictionary) -> Dictionary:
733733
if margins_v != null:
734734
if not (margins_v is Dictionary):
735735
return McpErrorCodes.make(
736-
McpErrorCodes.INVALID_PARAMS,
736+
McpErrorCodes.WRONG_TYPE,
737737
"drag_margins must be a dict with optional keys left/top/right/bottom"
738738
)
739739
var margins: Dictionary = margins_v
@@ -753,7 +753,7 @@ func set_damping_2d(params: Dictionary) -> Dictionary:
753753

754754
if applied.is_empty():
755755
return McpErrorCodes.make(
756-
McpErrorCodes.INVALID_PARAMS,
756+
McpErrorCodes.MISSING_REQUIRED_PARAM,
757757
"No damping params specified; provide at least one of position_speed, rotation_speed, drag_margins, drag_horizontal_enabled, drag_vertical_enabled"
758758
)
759759

@@ -788,7 +788,7 @@ func follow_2d(params: Dictionary) -> Dictionary:
788788

789789
if type_str != "2d":
790790
return McpErrorCodes.make(
791-
McpErrorCodes.INVALID_PARAMS,
791+
McpErrorCodes.WRONG_TYPE,
792792
"camera_follow_2d requires a Camera2D (got %s)" % node.get_class()
793793
)
794794

@@ -800,7 +800,7 @@ func follow_2d(params: Dictionary) -> Dictionary:
800800
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, "Target not found: %s" % target_path)
801801
if not (target is Node2D) and target != scene_root:
802802
return McpErrorCodes.make(
803-
McpErrorCodes.INVALID_PARAMS,
803+
McpErrorCodes.WRONG_TYPE,
804804
"Follow target must be a Node2D (got %s)" % target.get_class()
805805
)
806806
if target == node:
@@ -906,7 +906,7 @@ func get_camera(params: Dictionary) -> Dictionary:
906906
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(camera_path, scene_root))
907907
if not _is_camera(node):
908908
return McpErrorCodes.make(
909-
McpErrorCodes.INVALID_PARAMS,
909+
McpErrorCodes.WRONG_TYPE,
910910
"Node %s is not a camera (got %s)" % [camera_path, node.get_class()]
911911
)
912912
resolved_via = "path"
@@ -1099,7 +1099,7 @@ func _resolve_camera(params: Dictionary) -> Dictionary:
10991099
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(node_path, scene_root))
11001100
if not _is_camera(node):
11011101
return McpErrorCodes.make(
1102-
McpErrorCodes.INVALID_PARAMS,
1102+
McpErrorCodes.WRONG_TYPE,
11031103
"Node %s is not a camera (got %s)" % [node_path, node.get_class()]
11041104
)
11051105
return {

plugin/addons/godot_ai/handlers/control_draw_recipe_handler.gd

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func control_draw_recipe(params: Dictionary) -> Dictionary:
2525
if path.is_empty():
2626
return McpErrorCodes.make(McpErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
2727
if typeof(ops_raw) != TYPE_ARRAY:
28-
return McpErrorCodes.make(McpErrorCodes.INVALID_PARAMS, "ops must be an Array")
28+
return McpErrorCodes.make(McpErrorCodes.WRONG_TYPE, "ops must be an Array")
2929

3030
var scene_root := EditorInterface.get_edited_scene_root()
3131
if scene_root == null:
@@ -36,7 +36,7 @@ func control_draw_recipe(params: Dictionary) -> Dictionary:
3636
return McpErrorCodes.make(McpErrorCodes.NODE_NOT_FOUND, McpScenePath.format_node_error(path, scene_root))
3737
if not node is Control:
3838
return McpErrorCodes.make(
39-
McpErrorCodes.INVALID_PARAMS,
39+
McpErrorCodes.WRONG_TYPE,
4040
"control_draw_recipe requires a Control node, got %s" % node.get_class()
4141
)
4242

@@ -98,7 +98,7 @@ func _coerce_ops(ops: Array) -> Dictionary:
9898
var op: Variant = ops[i]
9999
if typeof(op) != TYPE_DICTIONARY:
100100
return McpErrorCodes.make(
101-
McpErrorCodes.INVALID_PARAMS, "ops[%d] must be a dictionary" % i
101+
McpErrorCodes.WRONG_TYPE, "ops[%d] must be a dictionary" % i
102102
)
103103
var coerced := _coerce_single_op(op, i)
104104
if coerced.has("error"):
@@ -149,7 +149,7 @@ func _coerce_typed(value: Variant, prop_type: int, idx: int, kind: String, field
149149
if r.ok:
150150
return {"ok": true, "value": r.value}
151151
return McpErrorCodes.make(
152-
McpErrorCodes.INVALID_PARAMS, "ops[%d] (%s): invalid '%s'" % [idx, kind, field]
152+
McpErrorCodes.VALUE_OUT_OF_RANGE, "ops[%d] (%s): invalid '%s'" % [idx, kind, field]
153153
)
154154

155155

@@ -249,7 +249,7 @@ func _coerce_polyline_or_polygon(op: Dictionary, idx: int, kind: String) -> Dict
249249
)
250250
if typeof(op.points) != TYPE_ARRAY:
251251
return McpErrorCodes.make(
252-
McpErrorCodes.INVALID_PARAMS,
252+
McpErrorCodes.WRONG_TYPE,
253253
"ops[%d] (%s): 'points' must be an Array" % [idx, kind]
254254
)
255255
var points := PackedVector2Array()
@@ -284,12 +284,12 @@ func _coerce_polyline_or_polygon(op: Dictionary, idx: int, kind: String) -> Dict
284284
var c := UiHandler._coerce_for_type(op.color, TYPE_COLOR)
285285
if not c.ok:
286286
return McpErrorCodes.make(
287-
McpErrorCodes.INVALID_PARAMS, "ops[%d] (%s): invalid 'color'" % [idx, kind]
287+
McpErrorCodes.VALUE_OUT_OF_RANGE, "ops[%d] (%s): invalid 'color'" % [idx, kind]
288288
)
289289
out["color"] = c.value
290290
else:
291291
return McpErrorCodes.make(
292-
McpErrorCodes.INVALID_PARAMS,
292+
McpErrorCodes.MISSING_REQUIRED_PARAM,
293293
"ops[%d] (%s): missing 'color' or 'colors'" % [idx, kind]
294294
)
295295

0 commit comments

Comments
 (0)