Files
tactical-shooter/addons/godot_mcp/tools/project_tools.gd
T
shawn b0c83af092 Fresh start: replace with naxIO/netfox-cs-sample foundation
Complete replacement of the tactical-shooter project with the
netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built
with Godot 4 and netfox.

## What's new
- Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu
- 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP
- Bomb plant/defuse with 2 bombsites
- Flashbang & smoke grenades
- Proper netfox rollback netcode at 64 tick
- Network popup UI for host/join
- HUD, crosshair, round timer, scoreboard
- All netfox singletons registered as autoloads (works in exported builds)

## Architecture
- Listen-server (host from client, no dedicated server binary)
- Multiplayer-fps game lives at examples/multiplayer-fps/
- Netfox addons registered as autoloads for exported build compat
- Godot 4.7 with Forward+ renderer

## Removed
- Old headless-server architecture (client_main, server_main, player.gd, etc.)
- Custom netfox bootstrap with ENet fallback
- Old ChaffGames FPS template (2,420 lines, 844 KB)
- SimulationServer GDExtension stub
- Godot-jolt physics (netfox sample uses default Godot physics)
- Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc.
- Server browser API Python venv (87 MB)
- test_range map and modular assets

## Preserved
- Git history
- Server config at config/default_server_config.cfg
- Windows export preset
- Build directory (gitignored)

Co-authored-by: naxIO <naxIO@users.noreply.github.com>
2026-07-02 20:55:20 -04:00

745 lines
26 KiB
GDScript

@tool
extends Node
class_name ProjectTools
## Project configuration and debug tools for MCP.
## Handles: get_project_settings, list_settings, update_project_settings,
## get_input_map, configure_input_map, get_collision_layers,
## get_node_properties, setup_autoload,
## get_console_log, get_errors, clear_console_log,
## open_in_godot, scene_tree_dump
var _editor_plugin: EditorPlugin = null
# Cached reference to the editor Output panel's RichTextLabel.
var _editor_log_rtl: RichTextLabel = null
# Character offset for clear_console_log.
var _clear_char_offset: int = 0
func set_editor_plugin(plugin: EditorPlugin) -> void:
_editor_plugin = plugin
# =============================================================================
# get_project_settings
# =============================================================================
func get_project_settings(args: Dictionary) -> Dictionary:
var include_render: bool = bool(args.get(&"include_render", true))
var include_physics: bool = bool(args.get(&"include_physics", true))
var out: Dictionary = {}
out[&"main_scene"] = str(ProjectSettings.get_setting("application/run/main_scene", ""))
# Window size
var width = ProjectSettings.get_setting("display/window/size/viewport_width", null)
var height = ProjectSettings.get_setting("display/window/size/viewport_height", null)
if width != null: out[&"window_width"] = int(width)
if height != null: out[&"window_height"] = int(height)
# Stretch
var stretch_mode = ProjectSettings.get_setting("display/window/stretch/mode", null)
var stretch_aspect = ProjectSettings.get_setting("display/window/stretch/aspect", null)
if stretch_mode != null: out[&"stretch_mode"] = str(stretch_mode)
if stretch_aspect != null: out[&"stretch_aspect"] = str(stretch_aspect)
if include_physics:
var pps = ProjectSettings.get_setting("physics/common/physics_ticks_per_second", null)
if pps != null: out[&"physics_ticks_per_second"] = int(pps)
if include_render:
var method = ProjectSettings.get_setting("rendering/renderer/rendering_method", null)
if method != null: out[&"rendering_method"] = str(method)
var vsync = ProjectSettings.get_setting("display/window/vsync/vsync_mode", null)
if vsync != null: out[&"vsync"] = str(vsync)
return {&"ok": true, &"settings": out}
# =============================================================================
# list_settings
# =============================================================================
func list_settings(args: Dictionary) -> Dictionary:
var category: String = str(args.get(&"category", ""))
var properties: Array = ProjectSettings.get_property_list()
if category.strip_edges().is_empty():
var categories: Dictionary = {}
for prop: Dictionary in properties:
var prop_name: String = prop[&"name"]
if prop_name.is_empty() or prop_name.begins_with("_"):
continue
var slash_idx := prop_name.find("/")
if slash_idx == -1:
continue
var cat: String = prop_name.substr(0, slash_idx)
categories[cat] = categories.get(cat, 0) + 1
return {&"ok": true, &"categories": categories,
&"hint": "Pass a category name to list its settings with current values and valid options."}
var settings: Array = []
for prop: Dictionary in properties:
var prop_name: String = prop[&"name"]
if not prop_name.begins_with(category + "/"):
continue
if prop_name.begins_with("_"):
continue
var info: Dictionary = {
&"path": prop_name,
&"type": _type_to_string(prop[&"type"]),
&"value": _serialize_value(ProjectSettings.get_setting(prop_name))
}
var hint: int = prop.get(&"hint", 0)
var hint_string: String = str(prop.get(&"hint_string", ""))
if hint == PROPERTY_HINT_ENUM and not hint_string.is_empty():
info[&"enum_values"] = hint_string
elif hint == PROPERTY_HINT_RANGE and not hint_string.is_empty():
info[&"range"] = hint_string
settings.append(info)
return {&"ok": true, &"category": category, &"settings": settings, &"count": settings.size()}
# =============================================================================
# update_project_settings
# =============================================================================
func update_project_settings(args: Dictionary) -> Dictionary:
var settings = args.get(&"settings", {})
if not settings is Dictionary or settings.is_empty():
return {&"ok": false, &"error": "Missing or empty 'settings' dictionary. Use list_settings to discover available setting paths."}
var updated: Array = []
for key: String in settings:
ProjectSettings.set_setting(key, settings[key])
updated.append(key)
_save_and_refresh_settings()
return {&"ok": true, &"updated": updated, &"count": updated.size()}
# =============================================================================
# get_input_map
# =============================================================================
func get_input_map(args: Dictionary) -> Dictionary:
var include_deadzones: bool = bool(args.get(&"include_deadzones", true))
var actions: Array = InputMap.get_actions()
actions.sort()
var result: Dictionary = {}
for action: StringName in actions:
var events: Array = []
for e: InputEvent in InputMap.action_get_events(action):
var item := {&"type": e.get_class()}
if e is InputEventKey:
var keycode = e.physical_keycode if e.physical_keycode != 0 else e.keycode
item[&"keycode"] = keycode
item[&"key_label"] = OS.get_keycode_string(keycode) if keycode != 0 else ""
elif e is InputEventMouseButton:
item[&"button_index"] = e.button_index
elif e is InputEventJoypadButton:
item[&"button_index"] = e.button_index
elif e is InputEventJoypadMotion:
item[&"axis"] = e.axis
if include_deadzones:
item[&"axis_value"] = e.axis_value
events.append(item)
result[action] = events
return {&"ok": true, &"actions": result, &"count": result.size()}
# =============================================================================
# configure_input_map
# =============================================================================
func configure_input_map(args: Dictionary) -> Dictionary:
var action: String = str(args.get(&"action", ""))
var operation: String = str(args.get(&"operation", ""))
if action.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'action' name"}
if operation.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'operation'. Use: add, remove, set"}
match operation:
"add":
return _input_map_add(action, args)
"remove":
return _input_map_remove(action)
"set":
return _input_map_set(action, args)
_:
return {&"ok": false, &"error": "Unknown operation: %s. Use: add, remove, set" % operation}
func _input_map_add(action: String, args: Dictionary) -> Dictionary:
var deadzone: float = float(args.get(&"deadzone", 0.5))
var events_data: Array = args.get(&"events", [])
var created := false
if not InputMap.has_action(action):
InputMap.add_action(action, deadzone)
created = true
var added_events: Array = []
var event_errors: Array = []
for event_desc in events_data:
if not event_desc is Dictionary:
continue
var result: Dictionary = _create_input_event(event_desc)
if result.has(&"error"):
event_errors.append(result[&"error"])
continue
InputMap.action_add_event(action, result[&"event"])
added_events.append(_describe_event(result[&"event"]))
_persist_action(action)
_save_and_refresh_settings()
_try_refresh_input_map_ui()
var msg := "Action '%s' %s" % [action, "created" if created else "updated"]
if added_events.size() > 0:
msg += " with %d event(s)" % added_events.size()
var out: Dictionary = {&"ok": true, &"message": msg, &"events_added": added_events}
if event_errors.size() > 0:
out[&"event_errors"] = event_errors
return out
func _input_map_remove(action: String) -> Dictionary:
if not InputMap.has_action(action):
return {&"ok": false, &"error": "Action not found: " + action}
InputMap.erase_action(action)
if ProjectSettings.has_setting("input/" + action):
ProjectSettings.clear("input/" + action)
_save_and_refresh_settings()
_try_refresh_input_map_ui()
return {&"ok": true, &"message": "Removed action: " + action}
func _input_map_set(action: String, args: Dictionary) -> Dictionary:
var deadzone: float = float(args.get(&"deadzone", 0.5))
var events_data: Array = args.get(&"events", [])
if InputMap.has_action(action):
InputMap.erase_action(action)
InputMap.add_action(action, deadzone)
var added_events: Array = []
var event_errors: Array = []
for event_desc in events_data:
if not event_desc is Dictionary:
continue
var result: Dictionary = _create_input_event(event_desc)
if result.has(&"error"):
event_errors.append(result[&"error"])
continue
InputMap.action_add_event(action, result[&"event"])
added_events.append(_describe_event(result[&"event"]))
_persist_action(action)
_save_and_refresh_settings()
_try_refresh_input_map_ui()
var out: Dictionary = {&"ok": true, &"message": "Set action '%s' with %d event(s)" % [action, added_events.size()], &"events": added_events}
if event_errors.size() > 0:
out[&"event_errors"] = event_errors
return out
func _create_input_event(desc: Dictionary) -> Dictionary:
var type: String = str(desc.get(&"type", ""))
match type:
"key":
var key_string: String = str(desc.get(&"key", ""))
if key_string.is_empty():
return {&"error": "Missing 'key' for key event"}
var event := InputEventKey.new()
var keycode := OS.find_keycode_from_string(key_string)
if keycode == 0:
return {&"error": "Unknown key: " + key_string}
event.physical_keycode = keycode
return {&"event": event}
"mouse_button":
var button_index: int = int(desc.get(&"button_index", 0))
if button_index <= 0:
return {&"error": "Invalid 'button_index' for mouse_button (must be >= 1: 1=left, 2=right, 3=middle)"}
var event := InputEventMouseButton.new()
event.button_index = button_index
return {&"event": event}
"joypad_button":
var button_index: int = int(desc.get(&"button_index", -1))
if button_index < 0:
return {&"error": "Missing or invalid 'button_index' for joypad_button"}
var event := InputEventJoypadButton.new()
event.button_index = button_index
return {&"event": event}
"joypad_motion":
var axis: int = int(desc.get(&"axis", -1))
if axis < 0:
return {&"error": "Missing or invalid 'axis' for joypad_motion"}
var axis_value: float = float(desc.get(&"axis_value", 0.0))
var event := InputEventJoypadMotion.new()
event.axis = axis
event.axis_value = axis_value
return {&"event": event}
_:
return {&"error": "Unknown event type: '%s'. Use: key, mouse_button, joypad_button, joypad_motion" % type}
func _save_and_refresh_settings() -> void:
ProjectSettings.save()
ProjectSettings.notify_property_list_changed()
func _try_refresh_input_map_ui() -> void:
if not _editor_plugin:
return
var base := _editor_plugin.get_editor_interface().get_base_control()
var pse := _find_node_by_class(base, "ProjectSettingsEditor")
if not pse:
return
if pse.has_method("_update_action_map_editor"):
pse.call("_update_action_map_editor")
else:
push_warning("[Godot MCP] Input map changed and saved, but the editor UI could not refresh. Reopen Project Settings to see changes.")
func _persist_action(action: String) -> void:
if not InputMap.has_action(action):
return
var deadzone: float = InputMap.action_get_deadzone(action)
var events: Array = InputMap.action_get_events(action)
ProjectSettings.set_setting("input/" + action, {
"deadzone": deadzone,
"events": events
})
func _describe_event(event: InputEvent) -> String:
if event is InputEventKey:
var keycode: int = event.physical_keycode if event.physical_keycode != 0 else event.keycode
var label: String = OS.get_keycode_string(keycode) if keycode != 0 else "Unknown"
return "Key: " + label
elif event is InputEventMouseButton:
return "Mouse Button: " + str(event.button_index)
elif event is InputEventJoypadButton:
return "Joypad Button: " + str(event.button_index)
elif event is InputEventJoypadMotion:
return "Joypad Axis: %d (%.1f)" % [event.axis, event.axis_value]
return event.get_class()
# =============================================================================
# get_collision_layers
# =============================================================================
func get_collision_layers(_args: Dictionary) -> Dictionary:
var layers_2d: Array = _collect_layers("layer_names/2d_physics")
var layers_3d: Array = _collect_layers("layer_names/3d_physics")
return {&"ok": true, &"layers_2d": layers_2d, &"layers_3d": layers_3d}
func _collect_layers(prefix: String) -> Array:
var out: Array = []
for i: int in range(1, 33):
var key := "%s/layer_%d" % [prefix, i]
var layer_name := str(ProjectSettings.get_setting(key, ""))
if not layer_name.is_empty():
out.append({&"index": i, &"name": layer_name})
return out
# =============================================================================
# get_node_properties
# =============================================================================
const _SKIP_PROPS: Dictionary = {
"script": true, "owner": true, "scene_file_path": true, "unique_name_in_owner": true,
}
const ENUM_HINTS = {
"anchors_preset": "0:Top Left,1:Top Right,2:Bottom Right,3:Bottom Left,4:Center Left,5:Center Top,6:Center Right,7:Center Bottom,8:Center,9:Left Wide,10:Top Wide,11:Right Wide,12:Bottom Wide,13:VCenter Wide,14:HCenter Wide,15:Full Rect",
"grow_horizontal": "0:Begin,1:End,2:Both",
"grow_vertical": "0:Begin,1:End,2:Both",
"horizontal_alignment": "0:Left,1:Center,2:Right,3:Fill",
"vertical_alignment": "0:Top,1:Center,2:Bottom,3:Fill"
}
func get_node_properties(args: Dictionary) -> Dictionary:
var node_type: String = str(args.get(&"node_type", ""))
if node_type.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'node_type'"}
if not ClassDB.class_exists(node_type):
return {&"ok": false, &"error": "Unknown node type: " + node_type}
var temp = ClassDB.instantiate(node_type)
if not temp:
return {&"ok": false, &"error": "Cannot instantiate: " + node_type}
var properties: Array = []
for prop: Dictionary in temp.get_property_list():
var prop_name: String = prop[&"name"]
if prop_name.begins_with("_"):
continue
if _SKIP_PROPS.has(prop_name):
continue
if not (prop.get(&"usage", 0) & PROPERTY_USAGE_EDITOR):
continue
var info := {
&"name": prop_name,
&"type": _type_to_string(prop[&"type"]),
&"default": _serialize_value(temp.get(prop_name))
}
# Enum hints
if prop.has(&"hint") and prop[&"hint"] == PROPERTY_HINT_ENUM and prop.has(&"hint_string"):
info[&"enum_values"] = prop[&"hint_string"]
if prop_name in ENUM_HINTS:
info[&"enum_values"] = ENUM_HINTS[prop_name]
properties.append(info)
temp.queue_free()
# Inheritance chain
var chain: Array = []
var cls: String = node_type
while cls != "":
chain.append(cls)
cls = ClassDB.get_parent_class(cls)
return {&"ok": true, &"node_type": node_type, &"inheritance_chain": chain,
&"property_count": properties.size(), &"properties": properties}
func _type_to_string(type_id: int) -> String:
match type_id:
TYPE_BOOL: return "bool"
TYPE_INT: return "int"
TYPE_FLOAT: return "float"
TYPE_STRING: return "String"
TYPE_VECTOR2: return "Vector2"
TYPE_VECTOR3: return "Vector3"
TYPE_VECTOR2I: return "Vector2i"
TYPE_VECTOR3I: return "Vector3i"
TYPE_COLOR: return "Color"
TYPE_RECT2: return "Rect2"
TYPE_OBJECT: return "Resource"
TYPE_ARRAY: return "Array"
TYPE_DICTIONARY: return "Dictionary"
_: return "Variant"
func _serialize_value(value: Variant) -> Variant:
match typeof(value):
TYPE_VECTOR2: return {&"type": &"Vector2", &"x": value.x, &"y": value.y}
TYPE_VECTOR3: return {&"type": &"Vector3", &"x": value.x, &"y": value.y, &"z": value.z}
TYPE_COLOR: return {&"type": &"Color", &"r": value.r, &"g": value.g, &"b": value.b, &"a": value.a}
TYPE_VECTOR2I: return {&"type": &"Vector2i", &"x": value.x, &"y": value.y}
TYPE_VECTOR3I: return {&"type": &"Vector3i", &"x": value.x, &"y": value.y, &"z": value.z}
TYPE_OBJECT:
if value and value is Resource and value.resource_path:
return {&"type": &"Resource", &"path": value.resource_path}
return null
_: return value
# =============================================================================
# setup_autoload
# =============================================================================
func setup_autoload(args: Dictionary) -> Dictionary:
var operation: String = str(args.get(&"operation", ""))
if operation.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'operation'. Use: add, remove, list"}
match operation:
"list":
return _autoload_list()
"add":
return _autoload_add(args)
"remove":
return _autoload_remove(args)
_:
return {&"ok": false, &"error": "Unknown operation: %s. Use: add, remove, list" % operation}
func _autoload_list() -> Dictionary:
var autoloads: Array = []
for prop: Dictionary in ProjectSettings.get_property_list():
var prop_name: String = prop[&"name"]
if not prop_name.begins_with("autoload/"):
continue
var al_name: String = prop_name.substr(9)
var al_path: String = str(ProjectSettings.get_setting(prop_name, ""))
var enabled: bool = al_path.begins_with("*")
if enabled:
al_path = al_path.substr(1)
autoloads.append({&"name": al_name, &"path": al_path, &"enabled": enabled})
return {&"ok": true, &"autoloads": autoloads, &"count": autoloads.size()}
func _autoload_add(args: Dictionary) -> Dictionary:
var autoload_name: String = str(args.get(&"name", ""))
var path: String = str(args.get(&"path", ""))
if autoload_name.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'name'"}
if path.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'path' for add operation"}
if not path.begins_with("res://"):
path = "res://" + path
if not FileAccess.file_exists(path):
return {&"ok": false, &"error": "File not found: " + path}
var setting_key := "autoload/" + autoload_name
ProjectSettings.set_setting(setting_key, "*" + path)
_save_and_refresh_settings()
return {&"ok": true, &"message": "Registered autoload: %s -> %s" % [autoload_name, path]}
func _autoload_remove(args: Dictionary) -> Dictionary:
var autoload_name: String = str(args.get(&"name", ""))
if autoload_name.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'name'"}
var setting_key := "autoload/" + autoload_name
if not ProjectSettings.has_setting(setting_key):
return {&"ok": false, &"error": "Autoload not found: " + autoload_name}
ProjectSettings.clear(setting_key)
_save_and_refresh_settings()
return {&"ok": true, &"message": "Unregistered autoload: " + autoload_name}
# =============================================================================
# Editor Output Panel access
# =============================================================================
# We read directly from the editor's internal EditorLog RichTextLabel.
# This is real-time and matches exactly what the user sees in the Output panel.
# =============================================================================
func _get_editor_log_rtl() -> RichTextLabel:
"""Find (and cache) the RichTextLabel inside the editor's Output panel."""
if is_instance_valid(_editor_log_rtl):
return _editor_log_rtl
if not _editor_plugin:
return null
var base := _editor_plugin.get_editor_interface().get_base_control()
var editor_log := _find_node_by_class(base, "EditorLog")
if editor_log:
_editor_log_rtl = _find_child_rtl(editor_log)
return _editor_log_rtl
func _find_node_by_class(root: Node, cls_name: String) -> Node:
if root.get_class() == cls_name:
return root
for child: Node in root.get_children():
var found := _find_node_by_class(child, cls_name)
if found:
return found
return null
func _find_child_rtl(node: Node) -> RichTextLabel:
for child: Node in node.get_children():
if child is RichTextLabel:
return child
var found := _find_child_rtl(child)
if found:
return found
return null
func _read_output_panel_lines() -> Array:
"""Return all non-empty lines from the editor Output panel (after clear offset)."""
var rtl := _get_editor_log_rtl()
if not rtl:
return []
var full_text: String = rtl.get_parsed_text()
if _clear_char_offset > 0 and _clear_char_offset < full_text.length():
full_text = full_text.substr(_clear_char_offset)
elif _clear_char_offset >= full_text.length():
return []
var lines: Array = []
for line: String in full_text.split("\n"):
if not line.strip_edges().is_empty():
lines.append(line)
return lines
# =============================================================================
# get_console_log
# =============================================================================
func get_console_log(args: Dictionary) -> Dictionary:
var max_lines: int = int(args.get(&"max_lines", 50))
var rtl := _get_editor_log_rtl()
if not rtl:
return {&"ok": false,
&"error": "Could not access the Godot editor Output panel. Make sure the MCP plugin is enabled and running inside the Godot editor."}
var all_lines := _read_output_panel_lines()
var start := maxi(0, all_lines.size() - max_lines)
var lines := all_lines.slice(start)
return {&"ok": true, &"lines": lines, &"line_count": lines.size(),
&"content": "\n".join(lines)}
# =============================================================================
# get_errors
# =============================================================================
const _ERROR_PREFIXES: PackedStringArray = [
"ERROR:", "SCRIPT ERROR:", "USER ERROR:",
"WARNING:", "USER WARNING:", "SCRIPT WARNING:",
"Parse Error:", "Invalid",
]
func get_errors(args: Dictionary) -> Dictionary:
var max_errors: int = int(args.get(&"max_errors", 50))
var include_warnings: bool = bool(args.get(&"include_warnings", true))
var rtl := _get_editor_log_rtl()
if not rtl:
return {&"ok": false,
&"error": "Could not access the Godot editor Output panel. Make sure the MCP plugin is enabled and running inside the Godot editor."}
var all_lines := _read_output_panel_lines()
var all_errors: Array = []
for i: int in range(all_lines.size()):
var line: String = all_lines[i].strip_edges()
if line.is_empty():
continue
var is_error := false
var severity := "error"
for prefix: String in _ERROR_PREFIXES:
if line.begins_with(prefix):
is_error = true
if "WARNING" in prefix:
severity = "warning"
break
# Godot continuation lines: "at: res://path/file.gd:123"
if not is_error and line.begins_with("at: ") and "res://" in line:
if all_errors.size() > 0:
var prev: Dictionary = all_errors[all_errors.size() - 1]
var loc := _extract_file_line(line)
if not loc.is_empty():
prev[&"file"] = loc.get(&"file", "")
prev[&"line"] = loc.get(&"line", 0)
continue
if not is_error:
continue
if severity == "warning" and not include_warnings:
continue
var error_info := {&"message": line, &"severity": severity}
var loc := _extract_file_line(line)
if not loc.is_empty():
error_info[&"file"] = loc.get(&"file", "")
error_info[&"line"] = loc.get(&"line", 0)
all_errors.append(error_info)
# Return the most recent errors
var start := maxi(0, all_errors.size() - max_errors)
var errors := all_errors.slice(start)
return {&"ok": true, &"errors": errors, &"error_count": errors.size(),
&"summary": "%d error(s) found" % errors.size()}
func _extract_file_line(text: String) -> Dictionary:
var idx := text.find("res://")
if idx == -1:
return {}
var rest := text.substr(idx)
var colon_idx := rest.find(":", 6)
if colon_idx == -1:
return {&"file": rest.strip_edges()}
var file_path := rest.substr(0, colon_idx)
var after_colon := rest.substr(colon_idx + 1)
var line_str := ""
for c in after_colon:
if c.is_valid_int():
line_str += c
else:
break
if not line_str.is_empty():
return {&"file": file_path, &"line": int(line_str)}
return {&"file": file_path}
# =============================================================================
# clear_console_log
# =============================================================================
func clear_console_log(_args: Dictionary) -> Dictionary:
var rtl := _get_editor_log_rtl()
if not rtl:
return {&"ok": false,
&"error": "Could not access the Godot editor Output panel. Make sure the MCP plugin is enabled and running inside the Godot editor."}
# Actually clear the editor Output panel
rtl.clear()
_clear_char_offset = 0
return {&"ok": true,
&"message": "Console log cleared."}
# =============================================================================
# open_in_godot
# =============================================================================
func open_in_godot(args: Dictionary) -> Dictionary:
var path: String = str(args.get(&"path", ""))
var line: int = int(args.get(&"line", 0))
if path.strip_edges().is_empty():
return {&"ok": false, &"error": "Missing 'path'"}
if not path.begins_with("res://"):
path = "res://" + path
if not _editor_plugin:
return {&"ok": false, &"error": "Editor plugin not available"}
var ei = _editor_plugin.get_editor_interface()
if path.ends_with(".gd") or path.ends_with(".shader"):
var script = load(path)
if script:
ei.edit_resource(script)
if line > 0:
ei.get_script_editor().goto_line(line - 1)
else:
return {&"ok": false, &"error": "Could not load: " + path}
elif path.ends_with(".tscn") or path.ends_with(".scn"):
ei.open_scene_from_path(path)
else:
var res = load(path)
if res:
ei.edit_resource(res)
return {&"ok": true, &"message": "Opened %s%s" % [path, " at line %d" % line if line > 0 else ""]}
# =============================================================================
# scene_tree_dump
# =============================================================================
func scene_tree_dump(_args: Dictionary) -> Dictionary:
if not _editor_plugin:
return {&"ok": false, &"error": "Editor plugin not available"}
var ei = _editor_plugin.get_editor_interface()
var edited_scene = ei.get_edited_scene_root()
if not edited_scene:
return {&"ok": true, &"tree": "(no scene open)", &"message": "No scene is currently open in the editor"}
var tree_text := _dump_node(edited_scene, 0)
return {&"ok": true, &"tree": tree_text, &"scene_path": edited_scene.scene_file_path}
func _dump_node(node: Node, depth: int) -> String:
var indent := " ".repeat(depth)
var line := "%s%s (%s)" % [indent, node.name, node.get_class()]
var script = node.get_script()
if script:
line += " [%s]" % script.resource_path.get_file()
var children := node.get_children()
if children.is_empty():
return line
var parts: PackedStringArray = [line]
for child: Node in children:
parts.append(_dump_node(child, depth + 1))
return "\n".join(parts)