Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions plugin/addons/godot_ai/handlers/audio_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ func set_stream(params: Dictionary) -> Dictionary:
if stream_path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: stream_path")

var stream_path_err = McpPathValidator.loadable_error(stream_path, "stream_path")
if stream_path_err != null:
return stream_path_err

var resolved := _resolve_player(player_path)
if resolved.has("error"):
return resolved
Expand Down Expand Up @@ -259,8 +263,9 @@ func list_streams(params: Dictionary) -> Dictionary:
var root: String = params.get("root", "res://")
var include_duration: bool = bool(params.get("include_duration", true))

if not root.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "root must start with res://")
var root_err = McpPathValidator.path_error(root, "root")
if root_err != null:
return root_err

var efs := EditorInterface.get_resource_filesystem()
if efs == null:
Expand Down
5 changes: 3 additions & 2 deletions plugin/addons/godot_ai/handlers/autoload_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ func add_autoload(params: Dictionary) -> Dictionary:
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: name")
if path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")
if not path.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Path must start with res:// (got: %s)" % path)
var path_err = McpPathValidator.path_error(path, "path")
if path_err != null:
return path_err
if not FileAccess.file_exists(path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)

Expand Down
3 changes: 3 additions & 0 deletions plugin/addons/godot_ai/handlers/curve_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ func set_points(params: Dictionary) -> Dictionary:
var node: Node = null
var curve_created := false
if has_file_target:
var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
if rpath_err != null:
return rpath_err
if not ResourceLoader.exists(resource_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % resource_path)
# ResourceLoader.load() returns Godot's cached Resource. Duplicate
Expand Down
12 changes: 6 additions & 6 deletions plugin/addons/godot_ai/handlers/filesystem_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
func read_file(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")

var path_err := McpPathValidator.validate_resource_path(path)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, path_err)
var path_err = McpPathValidator.path_error(path, "path")
if path_err != null:
return path_err

if not FileAccess.file_exists(path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
Expand All @@ -37,9 +37,9 @@ func write_file(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
var content: String = params.get("content", "")

var path_err := McpPathValidator.validate_resource_path(path)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, path_err)
var path_err = McpPathValidator.path_error(path, "path", true)
if path_err != null:
return path_err

# Ensure parent directory exists
var dir_path := path.get_base_dir()
Expand Down
32 changes: 20 additions & 12 deletions plugin/addons/godot_ai/handlers/material_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func create_material(params: Dictionary) -> Dictionary:
var shader_path: String = params.get("shader_path", "")
var overwrite: bool = params.get("overwrite", false)

var err := _validate_material_path(path, "path")
var err := _validate_material_path(path, "path", true)
if err != null:
return err

Expand All @@ -67,6 +67,9 @@ func create_material(params: Dictionary) -> Dictionary:
ErrorCodes.INVALID_PARAMS,
"ShaderMaterial requires shader_path (res:// path to a .gdshader)"
Comment thread
Copilot marked this conversation as resolved.
Outdated
)
var shader_path_err = McpPathValidator.loadable_error(shader_path, "shader_path")
if shader_path_err != null:
return shader_path_err
if not ResourceLoader.exists(shader_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Shader not found: %s" % shader_path)
var shader_res := ResourceLoader.load(shader_path)
Expand Down Expand Up @@ -111,7 +114,7 @@ func create_material(params: Dictionary) -> Dictionary:
# ============================================================================

func set_param(params: Dictionary) -> Dictionary:
var load_result := _load_material_from_path(params.get("path", ""))
var load_result := _load_material_from_path(params.get("path", ""), true)
if load_result.has("error"):
return load_result
var mat: Material = load_result.material
Expand Down Expand Up @@ -169,7 +172,7 @@ func set_param(params: Dictionary) -> Dictionary:
# ============================================================================

func set_shader_param(params: Dictionary) -> Dictionary:
var load_result := _load_material_from_path(params.get("path", ""))
var load_result := _load_material_from_path(params.get("path", ""), true)
if load_result.has("error"):
return load_result
var mat: Material = load_result.material
Expand Down Expand Up @@ -297,8 +300,9 @@ func list_materials(params: Dictionary) -> Dictionary:
var root: String = params.get("root", "res://")
var type_filter: String = params.get("type", "")

if not root.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "root must start with res://")
var root_err = McpPathValidator.path_error(root, "root")
if root_err != null:
return root_err

var efs := EditorInterface.get_resource_filesystem()
if efs == null:
Expand Down Expand Up @@ -366,6 +370,9 @@ func assign_material(params: Dictionary) -> Dictionary:
var mat: Material = null
var material_created := false
if not resource_path.is_empty():
var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
if rpath_err != null:
return rpath_err
if not ResourceLoader.exists(resource_path):
if create_if_missing:
# We'd need to create a new file here — refuse; callers should
Expand Down Expand Up @@ -463,7 +470,7 @@ func apply_to_node(params: Dictionary) -> Dictionary:
var save_to: String = params.get("save_to", "")
var saved := false
if not save_to.is_empty():
var save_err_validation := _validate_material_path(save_to, "save_to")
var save_err_validation := _validate_material_path(save_to, "save_to", true)
if save_err_validation != null:
return save_err_validation
var dir_path := save_to.get_base_dir()
Expand Down Expand Up @@ -564,7 +571,7 @@ func apply_preset(params: Dictionary) -> Dictionary:
"Material already exists at %s (pass overwrite=true to replace)" % path
)

var path_err := _validate_material_path(path, "path")
var path_err := _validate_material_path(path, "path", true)
if path_err != null:
return path_err

Expand Down Expand Up @@ -665,11 +672,12 @@ static func _reverse_type_map() -> Dictionary:
return out


static func _validate_material_path(path: String, param_name: String) -> Variant:
static func _validate_material_path(path: String, param_name: String, for_write: bool = false) -> Variant:
if path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: %s" % param_name)
if not path.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, "%s must start with res:// (got %s)" % [param_name, path])
var path_err := McpPathValidator.validate_resource_path(path, for_write)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "%s: %s" % [param_name, path_err])
var has_suffix := false
for s in _SUPPORTED_SUFFIXES:
if path.ends_with(s):
Expand All @@ -683,8 +691,8 @@ static func _validate_material_path(path: String, param_name: String) -> Variant
return null


func _load_material_from_path(path: String) -> Dictionary:
var err := _validate_material_path(path, "path")
func _load_material_from_path(path: String, for_write: bool = false) -> Dictionary:
var err := _validate_material_path(path, "path", for_write)
if err != null:
return err
if not ResourceLoader.exists(path):
Expand Down
5 changes: 3 additions & 2 deletions plugin/addons/godot_ai/handlers/material_values.gd
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,10 @@ static func parse_gradient(value: Variant) -> Variant:
return grad


## Load a Texture2D from a res:// path. Returns null on failure.
## Load a Texture2D from a res:// / uid:// / user:// path (validate_loadable_path).
## Returns null on failure (including a path that fails confinement / traversal).
static func load_texture(path: String) -> Texture2D:
if path.is_empty():
if not McpPathValidator.validate_loadable_path(path).is_empty():
return null
if not ResourceLoader.exists(path):
return null
Expand Down
8 changes: 6 additions & 2 deletions plugin/addons/godot_ai/handlers/node_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ func create_node(params: Dictionary) -> Dictionary:
# scene instance (foldout icon, the .tscn stores a reference instead of
# an exploded subtree). Descendants remain owned by their sub-scene;
# setting their owner to our scene_root would break the instance link.
if not scene_path.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "scene_path must start with res://")
var scene_path_err = McpPathValidator.loadable_error(scene_path, "scene_path")
if scene_path_err != null:
return scene_path_err
if not ResourceLoader.exists(scene_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Scene not found: %s" % scene_path)
var packed_scene = ResourceLoader.load(scene_path)
Expand Down Expand Up @@ -222,6 +223,9 @@ func set_property(params: Dictionary) -> Dictionary:
if value == "":
value = null
else:
var value_path_err = McpPathValidator.loadable_error(value, "value")
if value_path_err != null:
return value_path_err
if not ResourceLoader.exists(value):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % value)
var loaded := ResourceLoader.load(value)
Expand Down
15 changes: 15 additions & 0 deletions plugin/addons/godot_ai/handlers/particle_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@ func _set_draw_pass_gpu_3d(node: GPUParticles3D, node_path: String, pass_idx: in
if int(node.draw_passes) >= pass_idx:
existing_mesh = node.get(property_name) as Mesh
if not mesh_path.is_empty():
var mesh_path_err = McpPathValidator.loadable_error(mesh_path, "mesh_path")
if mesh_path_err != null:
return mesh_path_err
if not ResourceLoader.exists(mesh_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Mesh not found: %s" % mesh_path)
var loaded := ResourceLoader.load(mesh_path)
Expand All @@ -357,6 +360,9 @@ func _set_draw_pass_gpu_3d(node: GPUParticles3D, node_path: String, pass_idx: in

var material: Material = null
if not material_path.is_empty():
var material_path_err = McpPathValidator.loadable_error(material_path, "material_path")
if material_path_err != null:
return material_path_err
if not ResourceLoader.exists(material_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Material not found: %s" % material_path)
var loaded_mat := ResourceLoader.load(material_path)
Expand Down Expand Up @@ -407,6 +413,9 @@ func _set_draw_pass_cpu_3d(node: CPUParticles3D, node_path: String, mesh_path: S
var mesh: Mesh = node.mesh
var old_mesh: Mesh = mesh
if not mesh_path.is_empty():
var mesh_path_err = McpPathValidator.loadable_error(mesh_path, "mesh_path")
if mesh_path_err != null:
return mesh_path_err
if not ResourceLoader.exists(mesh_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Mesh not found: %s" % mesh_path)
var loaded := ResourceLoader.load(mesh_path)
Expand All @@ -417,6 +426,9 @@ func _set_draw_pass_cpu_3d(node: CPUParticles3D, node_path: String, mesh_path: S
var material: Material = null
var old_material: Material = node.material_override
if not material_path.is_empty():
var material_path_err = McpPathValidator.loadable_error(material_path, "material_path")
if material_path_err != null:
return material_path_err
if not ResourceLoader.exists(material_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Material not found: %s" % material_path)
var loaded_mat := ResourceLoader.load(material_path)
Expand Down Expand Up @@ -447,6 +459,9 @@ func _set_draw_pass_cpu_3d(node: CPUParticles3D, node_path: String, mesh_path: S
func _set_draw_pass_2d(node: Node, node_path: String, texture_path: String) -> Dictionary:
if texture_path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "2D particles require texture param")
var texture_path_err = McpPathValidator.loadable_error(texture_path, "texture_path")
if texture_path_err != null:
return texture_path_err
if not ResourceLoader.exists(texture_path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Texture not found: %s" % texture_path)
var tex := ResourceLoader.load(texture_path)
Expand Down
12 changes: 10 additions & 2 deletions plugin/addons/godot_ai/handlers/resource_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ func load_resource(params: Dictionary) -> Dictionary:
if path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")

if not path.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Path must start with res://")
var path_err = McpPathValidator.loadable_error(path, "path")
if path_err != null:
return path_err

if not ResourceLoader.exists(path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found: %s" % path)
Expand Down Expand Up @@ -112,6 +113,10 @@ func assign_resource(params: Dictionary) -> Dictionary:
if resource_path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: resource_path")

var rpath_err = McpPathValidator.loadable_error(resource_path, "resource_path")
if rpath_err != null:
return rpath_err

var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
if _resolved.has("error"):
return _resolved
Expand Down Expand Up @@ -248,6 +253,9 @@ static func _apply_resource_properties(res: Resource, properties: Dictionary) ->
if v == "":
v = null
else:
var vpath_err = McpPathValidator.loadable_error(v, "property '%s'" % key)
if vpath_err != null:
return vpath_err
var loaded := ResourceLoader.load(v)
if loaded == null:
return ErrorCodes.make(
Expand Down
14 changes: 10 additions & 4 deletions plugin/addons/godot_ai/handlers/scene_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ func create_scene(params: Dictionary) -> Dictionary:
if path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")

if not path.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Path must start with res://")
var path_err = McpPathValidator.path_error(path, "path", true)
if path_err != null:
return path_err

if not path.ends_with(".tscn") and not path.ends_with(".scn"):
path += ".tscn"
Expand Down Expand Up @@ -148,6 +149,10 @@ func open_scene(params: Dictionary) -> Dictionary:
if path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")

var path_err = McpPathValidator.loadable_error(path, "path")
if path_err != null:
return path_err

if not ResourceLoader.exists(path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "Scene not found: %s" % path)

Expand Down Expand Up @@ -202,8 +207,9 @@ func save_scene_as(params: Dictionary) -> Dictionary:
if path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: path")

if not path.begins_with("res://"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Path must start with res://")
var path_err = McpPathValidator.path_error(path, "path", true)
if path_err != null:
return path_err

if not path.ends_with(".tscn") and not path.ends_with(".scn"):
path += ".tscn"
Expand Down
28 changes: 16 additions & 12 deletions plugin/addons/godot_ai/handlers/script_handler.gd
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ func create_script(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")
var content: String = params.get("content", "")

var path_err := McpPathValidator.validate_resource_path(path)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, path_err)
var path_err = McpPathValidator.path_error(path, "path", true)
if path_err != null:
return path_err

if not path.ends_with(".gd"):
return ErrorCodes.make(ErrorCodes.VALUE_OUT_OF_RANGE, "Path must end with .gd")
Expand Down Expand Up @@ -141,9 +141,9 @@ static func _finish_create_script_deferred(
func read_script(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")

var path_err := McpPathValidator.validate_resource_path(path)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, path_err)
var path_err = McpPathValidator.path_error(path, "path")
if path_err != null:
return path_err

if not FileAccess.file_exists(path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
Expand Down Expand Up @@ -171,9 +171,9 @@ func patch_script(params: Dictionary) -> Dictionary:
var new_text: String = params.get("new_text", "")
var replace_all: bool = params.get("replace_all", false)

var path_err := McpPathValidator.validate_resource_path(path)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, path_err)
var path_err = McpPathValidator.path_error(path, "path", true)
if path_err != null:
return path_err
if not "old_text" in params:
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: old_text")
if not "new_text" in params:
Expand Down Expand Up @@ -241,6 +241,10 @@ func attach_script(params: Dictionary) -> Dictionary:
if script_path.is_empty():
return ErrorCodes.make(ErrorCodes.MISSING_REQUIRED_PARAM, "Missing required param: script_path")

var spath_err = McpPathValidator.loadable_error(script_path, "script_path")
if spath_err != null:
return spath_err

var _resolved := McpNodeValidator.resolve_or_error(node_path, "node_path")
if _resolved.has("error"):
return _resolved
Expand Down Expand Up @@ -304,9 +308,9 @@ func detach_script(params: Dictionary) -> Dictionary:
func find_symbols(params: Dictionary) -> Dictionary:
var path: String = params.get("path", "")

var path_err := McpPathValidator.validate_resource_path(path)
if not path_err.is_empty():
return ErrorCodes.make(ErrorCodes.INVALID_PARAMS, path_err)
var path_err = McpPathValidator.path_error(path, "path")
if path_err != null:
return path_err

if not FileAccess.file_exists(path):
return ErrorCodes.make(ErrorCodes.RESOURCE_NOT_FOUND, "File not found: %s" % path)
Expand Down
Loading
Loading