diff --git a/scripts/mcp_interaction_server.gd b/scripts/mcp_interaction_server.gd index 6ea8256..ca9de82 100644 --- a/scripts/mcp_interaction_server.gd +++ b/scripts/mcp_interaction_server.gd @@ -14,9 +14,18 @@ const BUSY_TIMEOUT: float = 30.0 var _key_map: Dictionary var _held_keys: Dictionary = {} +var _debugger_safe: bool = false + func _ready() -> void: - # Ensure MCP server keeps processing even when game is paused process_mode = Node.PROCESS_MODE_ALWAYS + if EngineDebugger.is_active(): + EngineDebugger.send_message("core:set_skip_breakpoints", [true]) + EngineDebugger.send_message("core:set_ignore_error_breaks", [true]) + _debugger_safe = EngineDebugger.is_skipping_breakpoints() + if _debugger_safe: + print("McpInteractionServer: Debugger breakpoints safely disabled") + else: + push_warning("McpInteractionServer: Could not disable debugger breakpoints (LocalDebugger?). Eval with invalid code may hang the game.") _init_key_map() _server = TCPServer.new() var err: int = _server.listen(PORT, "127.0.0.1") @@ -544,17 +553,18 @@ func _cmd_eval(params: Dictionary) -> void: _send_response({"error": "No code provided"}) return - # Wrap user code in a function so we can capture the return value - var script_source: String = """extends Node + var script_source: String = _build_eval_script(code) -func execute(): - var __result = null - __result = await _run() - return __result + if EngineDebugger.is_active() and not _debugger_safe: + var ext_validation: Dictionary = _validate_script_external(script_source) + if not ext_validation.get("valid", false): + _send_response({"error": "Script validation failed: %s" % ext_validation.get("error", "unknown error")}) + return -func _run(): -%s -""" % [_indent_code(code)] + var validation: Dictionary = _validate_script_source(script_source) + if not validation.get("valid", false): + _send_response({"error": "Script validation failed: %s" % validation.get("error", "unknown error"), "error_code": validation.get("error_code", -1)}) + return var script: GDScript = GDScript.new() script.source_code = script_source @@ -565,7 +575,6 @@ func _run(): var temp_node: Node = Node.new() temp_node.set_script(script) - # Allow eval to work even when game is paused temp_node.process_mode = Node.PROCESS_MODE_ALWAYS add_child(temp_node) @@ -577,6 +586,69 @@ func _run(): _send_response({"success": true, "result": _variant_to_json(result)}) +func _build_eval_script(code: String) -> String: + return """extends Node + +func execute(): + var __result = null + __result = await _run() + return __result + +func _run(): +%s +""" % [_indent_code(code)] + + +func _validate_script_source(source: String) -> Dictionary: + var test_script: GDScript = GDScript.new() + test_script.source_code = source + var err: int = test_script.reload() + if err != OK: + var error_name: String = "" + match err: + ERR_PARSE_ERROR: + error_name = "Parse error" + ERR_COMPILATION_FAILED: + error_name = "Compilation failed" + _: + error_name = "Error code %d" % err + return {"valid": false, "error": error_name, "error_code": err} + return {"valid": true} + + +func _validate_script_external(source: String) -> Dictionary: + var temp_path: String = "user://_mcp_eval_validate_%d.gd" % Time.get_ticks_msec() + var file: FileAccess = FileAccess.open(temp_path, FileAccess.WRITE) + if file == null: + return {"valid": true} + file.store_string(source) + file.close() + + var godot_path: String = OS.get_executable_path() + var project_path: String = ProjectSettings.globalize_path("res://") + var global_temp: String = ProjectSettings.globalize_path(temp_path) + var output: Array = [] + var exit_code: int = OS.execute(godot_path, [ + "--headless", + "--check-only", + "--script", global_temp, + "--path", project_path + ], output) + + DirAccess.remove_absolute(global_temp) + + if exit_code != 0: + var error_detail: String = "" + for line in output: + error_detail += line + "\n" + error_detail = error_detail.strip_edges() + var msg: String = "Syntax error in eval code (external validation)" + if not error_detail.is_empty(): + msg += ": " + error_detail + return {"valid": false, "error": msg} + return {"valid": true} + + func _indent_code(code: String) -> String: var lines: PackedStringArray = code.split("\n") var indented: String = ""