@tool extends "res://addons/godot_mcp/commands/base_command.gd" const COMMANDS_PATH := "user://mcp_input_commands" func get_commands() -> Dictionary: return { "simulate_key": _simulate_key, "simulate_mouse_click": _simulate_mouse_click, "simulate_mouse_move": _simulate_mouse_move, "simulate_action": _simulate_action, "simulate_sequence": _simulate_sequence, } func _simulate_key(params: Dictionary) -> Dictionary: var result := require_string(params, "keycode") if result[1] != null: return result[1] var keycode: String = result[0] var pressed: bool = optional_bool(params, "pressed", true) var shift: bool = optional_bool(params, "shift", false) var ctrl: bool = optional_bool(params, "ctrl", false) var alt: bool = optional_bool(params, "alt", false) var event := { "type": "key", "keycode": keycode, "pressed": pressed, "shift": shift, "ctrl": ctrl, "alt": alt, } _write_commands([event]) return success({"sent": true, "event": event}) func _simulate_mouse_click(params: Dictionary) -> Dictionary: var button: int = optional_int(params, "button", 1) # MOUSE_BUTTON_LEFT var pressed: bool = optional_bool(params, "pressed", true) var double_click: bool = optional_bool(params, "double_click", false) var auto_release: bool = optional_bool(params, "auto_release", true) var x: float = float(params.get("x", 0)) var y: float = float(params.get("y", 0)) var press_event := { "type": "mouse_button", "button": button, "pressed": pressed, "double_click": double_click, "position": {"x": x, "y": y}, } # Auto-release: send press + release in sequence so UI buttons actually fire if pressed and auto_release: var release_event := press_event.duplicate() release_event["pressed"] = false var sequence_data := { "sequence_events": [press_event, release_event], "frame_delay": 1, } var json := JSON.stringify(sequence_data) var file := FileAccess.open(COMMANDS_PATH, FileAccess.WRITE) if file == null: return error_internal("Failed to write commands: %s" % error_string(FileAccess.get_open_error())) file.store_string(json) file.close() return success({"sent": true, "event": press_event, "auto_release": true}) _write_commands([press_event]) return success({"sent": true, "event": press_event}) func _simulate_mouse_move(params: Dictionary) -> Dictionary: var x: float = float(params.get("x", 0)) var y: float = float(params.get("y", 0)) var rel_x: float = float(params.get("relative_x", 0)) var rel_y: float = float(params.get("relative_y", 0)) var button_mask: int = optional_int(params, "button_mask", 0) var unhandled_explicit: bool = params.has("unhandled") var unhandled: bool = optional_bool(params, "unhandled", false) var event := { "type": "mouse_motion", "position": {"x": x, "y": y}, "relative": {"x": rel_x, "y": rel_y}, "button_mask": button_mask, } # Auto-enable unhandled for drag motions (camera-pan use case) ONLY when # the caller did NOT explicitly pass an "unhandled" key. If they passed # one — true or false — honor it. This lets UI drag-and-drop tests opt # back into normal GUI dispatch by passing unhandled: false explicitly. if unhandled_explicit: event["unhandled"] = unhandled elif button_mask > 0: event["unhandled"] = true _write_commands([event]) return success({"sent": true, "event": event}) func _simulate_action(params: Dictionary) -> Dictionary: var result := require_string(params, "action") if result[1] != null: return result[1] var action_name: String = result[0] var pressed: bool = optional_bool(params, "pressed", true) var strength: float = float(params.get("strength", 1.0)) var event := { "type": "action", "action": action_name, "pressed": pressed, "strength": strength, } _write_commands([event]) return success({"sent": true, "event": event}) func _simulate_sequence(params: Dictionary) -> Dictionary: if not params.has("events") or not params["events"] is Array: return error_invalid_params("Missing required parameter: events (Array)") var events: Array = params["events"] if events.is_empty(): return error_invalid_params("Events array is empty") var frame_delay: int = optional_int(params, "frame_delay", 1) for event_data: Dictionary in events: if not event_data.has("type") or (event_data["type"] as String).is_empty(): return error_invalid_params("Invalid event in sequence: %s" % str(event_data)) if frame_delay <= 0: # All events in one frame - write as plain array _write_commands(events) else: # Sequence with frame delay - game side handles timing var sequence_data := { "sequence_events": events, "frame_delay": frame_delay, } var json := JSON.stringify(sequence_data) var file := FileAccess.open(COMMANDS_PATH, FileAccess.WRITE) if file == null: return error_internal("Failed to write commands: %s" % error_string(FileAccess.get_open_error())) file.store_string(json) file.close() return success({"sent": true, "event_count": events.size(), "frame_delay": frame_delay}) func _write_commands(events: Array) -> void: var json := JSON.stringify(events) var file := FileAccess.open(COMMANDS_PATH, FileAccess.WRITE) if file == null: push_error("[MCP Input] Failed to write commands: %s" % error_string(FileAccess.get_open_error())) return file.store_string(json) file.close()