@tool extends "res://addons/godot_mcp/commands/base_command.gd" func get_commands() -> Dictionary: return { "create_theme": _create_theme, "set_theme_color": _set_theme_color, "set_theme_constant": _set_theme_constant, "set_theme_font_size": _set_theme_font_size, "set_theme_stylebox": _set_theme_stylebox, "setup_control": _setup_control, "get_theme_info": _get_theme_info, } func _create_theme(params: Dictionary) -> Dictionary: var result := require_string(params, "path") if result[1] != null: return result[1] var path: String = result[0] var theme := Theme.new() # Optionally set default font size var font_size: int = optional_int(params, "default_font_size", 0) if font_size > 0: theme.default_font_size = font_size var scene_guard := guard_offline_scene_save(path) if scene_guard != null: return scene_guard var err := ResourceSaver.save(theme, path) if err != OK: return error_internal("Failed to save theme: %s" % error_string(err)) EditorInterface.get_resource_filesystem().scan() return success({"path": path, "created": true}) func _set_theme_color(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var result2 := require_string(params, "name") if result2[1] != null: return result2[1] var color_name: String = result2[0] var result3 := require_string(params, "color") if result3[1] != null: return result3[1] var color_str: String = result3[0] var node := find_node_by_path(node_path) if node == null or not (node is Control): return error_not_found("Control node at '%s'" % node_path) var control: Control = node var color := Color(color_str) var theme_type: String = optional_string(params, "theme_type", "") if theme_type.is_empty(): theme_type = control.get_class() var had_old := control.has_theme_color_override(color_name) var old_value: Variant = control.get("theme_override_colors/" + color_name) if had_old else null var undo_redo := get_undo_redo() undo_redo.create_action("MCP: Set theme color override") undo_redo.add_do_method(control, "add_theme_color_override", color_name, color) undo_redo.add_undo_method(self, "_restore_theme_override", control, "color", color_name, had_old, old_value) undo_redo.commit_action() return success({"node_path": node_path, "name": color_name, "color": color_str}) func _set_theme_constant(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var result2 := require_string(params, "name") if result2[1] != null: return result2[1] var const_name: String = result2[0] var node := find_node_by_path(node_path) if node == null or not (node is Control): return error_not_found("Control node at '%s'" % node_path) var control: Control = node var value: int = int(params.get("value", 0)) var had_old := control.has_theme_constant_override(const_name) var old_value: Variant = control.get("theme_override_constants/" + const_name) if had_old else null var undo_redo := get_undo_redo() undo_redo.create_action("MCP: Set theme constant override") undo_redo.add_do_method(control, "add_theme_constant_override", const_name, value) undo_redo.add_undo_method(self, "_restore_theme_override", control, "constant", const_name, had_old, old_value) undo_redo.commit_action() return success({"node_path": node_path, "name": const_name, "value": value}) func _set_theme_font_size(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var result2 := require_string(params, "name") if result2[1] != null: return result2[1] var font_name: String = result2[0] var node := find_node_by_path(node_path) if node == null or not (node is Control): return error_not_found("Control node at '%s'" % node_path) var control: Control = node var size: int = int(params.get("size", 16)) var had_old := control.has_theme_font_size_override(font_name) var old_value: Variant = control.get("theme_override_font_sizes/" + font_name) if had_old else null var undo_redo := get_undo_redo() undo_redo.create_action("MCP: Set theme font size override") undo_redo.add_do_method(control, "add_theme_font_size_override", font_name, size) undo_redo.add_undo_method(self, "_restore_theme_override", control, "font_size", font_name, had_old, old_value) undo_redo.commit_action() return success({"node_path": node_path, "name": font_name, "size": size}) func _set_theme_stylebox(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var result2 := require_string(params, "name") if result2[1] != null: return result2[1] var style_name: String = result2[0] var node := find_node_by_path(node_path) if node == null or not (node is Control): return error_not_found("Control node at '%s'" % node_path) var control: Control = node var stylebox := StyleBoxFlat.new() var bg_color: String = optional_string(params, "bg_color", "") if not bg_color.is_empty(): stylebox.bg_color = Color(bg_color) var border_color: String = optional_string(params, "border_color", "") if not border_color.is_empty(): stylebox.border_color = Color(border_color) var border_width: int = optional_int(params, "border_width", 0) if border_width > 0: stylebox.border_width_left = border_width stylebox.border_width_top = border_width stylebox.border_width_right = border_width stylebox.border_width_bottom = border_width var corner_radius: int = optional_int(params, "corner_radius", 0) if corner_radius > 0: stylebox.corner_radius_top_left = corner_radius stylebox.corner_radius_top_right = corner_radius stylebox.corner_radius_bottom_left = corner_radius stylebox.corner_radius_bottom_right = corner_radius var padding: int = optional_int(params, "padding", 0) if padding > 0: stylebox.content_margin_left = padding stylebox.content_margin_top = padding stylebox.content_margin_right = padding stylebox.content_margin_bottom = padding var had_old := control.has_theme_stylebox_override(style_name) var old_value: Variant = control.get("theme_override_styles/" + style_name) if had_old else null var undo_redo := get_undo_redo() undo_redo.create_action("MCP: Set theme stylebox override") undo_redo.add_do_method(control, "add_theme_stylebox_override", style_name, stylebox) undo_redo.add_do_reference(stylebox) undo_redo.add_undo_method(self, "_restore_theme_override", control, "stylebox", style_name, had_old, old_value) if old_value is Resource: undo_redo.add_undo_reference(old_value) undo_redo.commit_action() return success({"node_path": node_path, "name": style_name, "type": "StyleBoxFlat"}) func _setup_control(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var node := find_node_by_path(node_path) if node == null or not (node is Control): return error_not_found("Control node at '%s'" % node_path) var control: Control = node var applied: Array = [] var old_state := _capture_control_setup_state(control) var target: Control = control.duplicate() as Control # Anchor preset var anchor_preset: String = optional_string(params, "anchor_preset", "") if not anchor_preset.is_empty(): var preset_map := { "top_left": Control.PRESET_TOP_LEFT, "top_right": Control.PRESET_TOP_RIGHT, "bottom_left": Control.PRESET_BOTTOM_LEFT, "bottom_right": Control.PRESET_BOTTOM_RIGHT, "center_left": Control.PRESET_CENTER_LEFT, "center_top": Control.PRESET_CENTER_TOP, "center_right": Control.PRESET_CENTER_RIGHT, "center_bottom": Control.PRESET_CENTER_BOTTOM, "center": Control.PRESET_CENTER, "left_wide": Control.PRESET_LEFT_WIDE, "top_wide": Control.PRESET_TOP_WIDE, "right_wide": Control.PRESET_RIGHT_WIDE, "bottom_wide": Control.PRESET_BOTTOM_WIDE, "vcenter_wide": Control.PRESET_VCENTER_WIDE, "hcenter_wide": Control.PRESET_HCENTER_WIDE, "full_rect": Control.PRESET_FULL_RECT, } if preset_map.has(anchor_preset): target.set_anchors_and_offsets_preset(preset_map[anchor_preset]) applied.append("anchor_preset=%s" % anchor_preset) # Min size var min_size_str: String = optional_string(params, "min_size", "") if not min_size_str.is_empty(): var expr := Expression.new() if expr.parse(min_size_str) == OK: var val = expr.execute() if val is Vector2: target.custom_minimum_size = val applied.append("min_size=%s" % min_size_str) # Size flags horizontal var sf_h: String = optional_string(params, "size_flags_h", "") if not sf_h.is_empty(): var flags_map := { "fill": Control.SIZE_FILL, "expand": Control.SIZE_EXPAND, "fill_expand": Control.SIZE_EXPAND_FILL, "shrink_center": Control.SIZE_SHRINK_CENTER, "shrink_end": Control.SIZE_SHRINK_END, } if flags_map.has(sf_h): target.size_flags_horizontal = flags_map[sf_h] applied.append("size_flags_h=%s" % sf_h) # Size flags vertical var sf_v: String = optional_string(params, "size_flags_v", "") if not sf_v.is_empty(): var flags_map := { "fill": Control.SIZE_FILL, "expand": Control.SIZE_EXPAND, "fill_expand": Control.SIZE_EXPAND_FILL, "shrink_center": Control.SIZE_SHRINK_CENTER, "shrink_end": Control.SIZE_SHRINK_END, } if flags_map.has(sf_v): target.size_flags_vertical = flags_map[sf_v] applied.append("size_flags_v=%s" % sf_v) # Margins (for MarginContainer) if params.has("margins") and params["margins"] is Dictionary: var margins: Dictionary = params["margins"] if target is MarginContainer: if margins.has("left"): target.add_theme_constant_override("margin_left", int(margins["left"])) if margins.has("top"): target.add_theme_constant_override("margin_top", int(margins["top"])) if margins.has("right"): target.add_theme_constant_override("margin_right", int(margins["right"])) if margins.has("bottom"): target.add_theme_constant_override("margin_bottom", int(margins["bottom"])) applied.append("margins=%s" % str(margins)) # Separation (for VBox/HBoxContainer) if params.has("separation"): var sep: int = int(params["separation"]) if target is BoxContainer: target.add_theme_constant_override("separation", sep) applied.append("separation=%d" % sep) # Grow direction horizontal var grow_h: String = optional_string(params, "grow_h", "") if not grow_h.is_empty(): var grow_map := { "begin": Control.GROW_DIRECTION_BEGIN, "end": Control.GROW_DIRECTION_END, "both": Control.GROW_DIRECTION_BOTH, } if grow_map.has(grow_h): target.grow_horizontal = grow_map[grow_h] applied.append("grow_h=%s" % grow_h) # Grow direction vertical var grow_v: String = optional_string(params, "grow_v", "") if not grow_v.is_empty(): var grow_map := { "begin": Control.GROW_DIRECTION_BEGIN, "end": Control.GROW_DIRECTION_END, "both": Control.GROW_DIRECTION_BOTH, } if grow_map.has(grow_v): target.grow_vertical = grow_map[grow_v] applied.append("grow_v=%s" % grow_v) if not applied.is_empty(): var new_state := _capture_control_setup_state(target) _register_control_setup_undo(control, old_state, new_state) target.free() return success({"node_path": node_path, "applied": applied, "count": applied.size()}) func _restore_theme_override(control: Control, kind: String, override_name: String, had_old: bool, old_value: Variant) -> void: match kind: "color": if had_old: control.add_theme_color_override(override_name, old_value) else: control.remove_theme_color_override(override_name) "constant": if had_old: control.add_theme_constant_override(override_name, old_value) else: control.remove_theme_constant_override(override_name) "font_size": if had_old: control.add_theme_font_size_override(override_name, old_value) else: control.remove_theme_font_size_override(override_name) "stylebox": if had_old: control.add_theme_stylebox_override(override_name, old_value) else: control.remove_theme_stylebox_override(override_name) func _capture_control_setup_state(control: Control) -> Dictionary: var state := {"properties": {}, "theme_constants": {}} for property: String in [ "anchor_left", "anchor_top", "anchor_right", "anchor_bottom", "offset_left", "offset_top", "offset_right", "offset_bottom", "custom_minimum_size", "size_flags_horizontal", "size_flags_vertical", "grow_horizontal", "grow_vertical", ]: state["properties"][property] = control.get(property) for constant_name: String in ["margin_left", "margin_top", "margin_right", "margin_bottom", "separation"]: var had_override := control.has_theme_constant_override(constant_name) state["theme_constants"][constant_name] = { "had": had_override, "value": control.get("theme_override_constants/" + constant_name) if had_override else null, } return state func _register_control_setup_undo(control: Control, old_state: Dictionary, new_state: Dictionary) -> void: var undo_redo := get_undo_redo() undo_redo.create_action("MCP: Setup Control") for property: String in new_state["properties"]: undo_redo.add_do_property(control, property, new_state["properties"][property]) undo_redo.add_undo_property(control, property, old_state["properties"][property]) for constant_name: String in new_state["theme_constants"]: var new_constant: Dictionary = new_state["theme_constants"][constant_name] var old_constant: Dictionary = old_state["theme_constants"][constant_name] undo_redo.add_do_method(self, "_restore_theme_override", control, "constant", constant_name, new_constant["had"], new_constant["value"]) undo_redo.add_undo_method(self, "_restore_theme_override", control, "constant", constant_name, old_constant["had"], old_constant["value"]) undo_redo.commit_action() func _get_theme_info(params: Dictionary) -> Dictionary: var result := require_string(params, "node_path") if result[1] != null: return result[1] var node_path: String = result[0] var node := find_node_by_path(node_path) if node == null or not (node is Control): return error_not_found("Control node at '%s'" % node_path) var control: Control = node var info := {"node_path": node_path, "class": control.get_class()} # Check if node has a theme var theme := control.theme if theme: info["theme_path"] = theme.resource_path info["type_list"] = Array(theme.get_type_list()) # List overrides var overrides := {"colors": {}, "constants": {}, "font_sizes": {}, "styleboxes": {}} for prop in control.get_property_list(): var pname: String = prop["name"] if pname.begins_with("theme_override_colors/"): var key := pname.substr(22) overrides["colors"][key] = "#" + (control.get(pname) as Color).to_html() elif pname.begins_with("theme_override_constants/"): var key := pname.substr(25) overrides["constants"][key] = control.get(pname) elif pname.begins_with("theme_override_font_sizes/"): var key := pname.substr(26) overrides["font_sizes"][key] = control.get(pname) elif pname.begins_with("theme_override_styles/"): var key := pname.substr(22) var style = control.get(pname) overrides["styleboxes"][key] = style.get_class() if style else null info["overrides"] = overrides return success(info)