@tool extends EditorPlugin const _MCP_AUTOLOADS: Array[Array] = [ ["autoload/MCPScreenshot", "res://addons/godot_mcp/mcp_screenshot_service.gd"], ["autoload/MCPInputService", "res://addons/godot_mcp/mcp_input_service.gd"], ["autoload/MCPGameInspector", "res://addons/godot_mcp/mcp_game_inspector_service.gd"], ] const _MCP_TEMP_FILES: Array[String] = [ "mcp_game_request", "mcp_game_response", "mcp_input_commands", "mcp_screenshot_request", ] var websocket_server: Node var command_router: Node var status_panel: Control var auto_dismiss_dialogs: bool = false # Track which autoloads THIS session injected (vs project-owned) var _session_injected_autoloads: Array[String] = [] func _enter_tree() -> void: # Create command router command_router = preload("res://addons/godot_mcp/command_router.gd").new() command_router.name = "MCPCommandRouter" command_router.editor_plugin = self add_child(command_router) # Create WebSocket server websocket_server = preload("res://addons/godot_mcp/websocket_server.gd").new() websocket_server.name = "MCPWebSocketServer" websocket_server.command_router = command_router add_child(websocket_server) # Create status panel var panel_scene: PackedScene = preload("res://addons/godot_mcp/ui/status_panel.tscn") status_panel = panel_scene.instantiate() add_control_to_bottom_panel(status_panel, "MCP Pro") status_panel.call_deferred("setup", websocket_server, command_router) # Inject MCP autoloads into project settings _inject_autoloads() websocket_server.start_server() var cfg := ConfigFile.new() var ver := "unknown" if cfg.load("res://addons/godot_mcp/plugin.cfg") == OK: ver = cfg.get_value("plugin", "version", "unknown") print("[MCP] Godot MCP Pro v%s started (ports 6505-6514)" % ver) func _exit_tree() -> void: # Remove MCP autoloads and clean up temp files _remove_autoloads() _cleanup_temp_files() if websocket_server: websocket_server.stop_server() if status_panel: remove_control_from_bottom_panel(status_panel) status_panel.queue_free() if command_router: command_router.queue_free() if websocket_server: websocket_server.queue_free() print("[MCP] Godot MCP Pro stopped") func _inject_autoloads() -> void: _session_injected_autoloads.clear() var changed := false for entry: Array in _MCP_AUTOLOADS: var key: String = entry[0] var script: String = entry[1] if not ProjectSettings.has_setting(key): ProjectSettings.set_setting(key, "*" + script) _session_injected_autoloads.append(key) changed = true if changed: ProjectSettings.save() func _remove_autoloads() -> void: # Only remove autoloads that THIS session injected. # Pre-existing project-owned autoloads are preserved. var changed := false for key: String in _session_injected_autoloads: if ProjectSettings.has_setting(key): ProjectSettings.set_setting(key, null) changed = true _session_injected_autoloads.clear() if changed: ProjectSettings.save() var _dialog_check_timer: float = 0.0 const _DIALOG_CHECK_INTERVAL: float = 0.5 # Check every 0.5 seconds func _process(delta: float) -> void: # Check if game inspector requested debugger continue var flag_path := OS.get_user_data_dir() + "/mcp_debugger_continue" if FileAccess.file_exists(flag_path): DirAccess.remove_absolute(flag_path) _try_debugger_continue() # Periodically check for blocking editor dialogs (only when enabled by AI) if auto_dismiss_dialogs: _dialog_check_timer += delta if _dialog_check_timer >= _DIALOG_CHECK_INTERVAL: _dialog_check_timer = 0.0 _auto_dismiss_dialogs() func _try_debugger_continue() -> void: # Last resort: find and press the debugger Continue button to unstick the game var base: Node = EditorInterface.get_base_control() var continue_btn := _find_debugger_continue_button(base) if continue_btn and continue_btn.visible and not continue_btn.disabled: continue_btn.emit_signal("pressed") push_warning("[MCP] Auto-pressed debugger Continue button") else: push_warning("[MCP] Could not find debugger Continue button") func _find_debugger_continue_button(node: Node) -> Button: # Search for the Continue button in ScriptEditorDebugger if node is Button: var btn: Button = node if btn.tooltip_text.contains("Continue") or btn.text == "Continue": return btn for child in node.get_children(): var found: Button = _find_debugger_continue_button(child) if found: return found return null func _auto_dismiss_dialogs() -> void: var base: Node = EditorInterface.get_base_control() if not base: return _find_and_dismiss_dialogs(base) func _find_and_dismiss_dialogs(node: Node) -> void: if node is AcceptDialog and node.visible: var dialog: AcceptDialog = node # Never dismiss file dialogs or non-modal popups if dialog is FileDialog: return if not dialog.exclusive: return # Get dialog title/text for logging var title := dialog.title var text := dialog.dialog_text # Accept the dialog (presses OK / confirms) dialog.get_ok_button().emit_signal("pressed") push_warning("[MCP] Auto-dismissed editor dialog: '%s' — %s" % [title, text]) return # One dialog per check cycle to avoid side effects for child in node.get_children(): # Only search visible Windows to keep the scan lightweight if child is Window and not child.visible: continue _find_and_dismiss_dialogs(child) func _cleanup_temp_files() -> void: var user_dir := OS.get_user_data_dir() for filename: String in _MCP_TEMP_FILES: var path := user_dir + "/" + filename if FileAccess.file_exists(path): DirAccess.remove_absolute(path) # Also clean up screenshot image var screenshot_path := user_dir + "/mcp_screenshot.png" if FileAccess.file_exists(screenshot_path): DirAccess.remove_absolute(screenshot_path)