Files
server-deploy/windows-dev-stack/godot-mcp-pro-v1.14.1/addons/godot_mcp/plugin.gd
Joywayer dd3eb24d0f refactor: 拆分 claude-dev-stack 为 windows-dev-stack 和 wsl-dev-stack
将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-29 01:11:20 +08:00

184 lines
5.6 KiB
GDScript

@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)