Stealth Cymbal #3

Merged
notid merged 10 commits from stealth-cymbal into master 2026-04-29 16:56:44 -07:00
Showing only changes of commit 7a7d9e78db - Show all commits

View File

@@ -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 = ""