133 lines
4.9 KiB
GDScript
133 lines
4.9 KiB
GDScript
@tool
|
|
extends EditorPlugin
|
|
|
|
const Server := preload("res://addons/agent_tools/server.gd")
|
|
const Registry := preload("res://addons/agent_tools/registry.gd")
|
|
|
|
const DEFAULT_PORT := 9920
|
|
const PORT_RANGE_END := 9929 # scan 9920..9929 inclusive
|
|
const PORT_SETTING := "agent_tools/port"
|
|
const BRIDGE_AUTOLOAD_NAME := "_MCPGameBridge"
|
|
const BRIDGE_AUTOLOAD_PATH := "res://addons/agent_tools/runtime/game_bridge.gd"
|
|
|
|
const DEFAULT_INTERFACE := "127.0.0.1"
|
|
const INTERFACE_SETTING := "agent_tools/interface"
|
|
|
|
var _server: Server
|
|
var _registry: Registry
|
|
var _bound_port: int = -1
|
|
var _session_file_path: String = ""
|
|
|
|
|
|
func _enter_tree() -> void:
|
|
_registry = Registry.new()
|
|
_server = Server.new(_registry)
|
|
add_child(_server)
|
|
|
|
# Interface resolution
|
|
# Defaults to 127.0.0.1, override with project.godot agent_tools/interface
|
|
var forced_interface := DEFAULT_INTERFACE
|
|
if ProjectSettings.has_setting(INTERFACE_SETTING):
|
|
forced_interface = ProjectSettings.get_setting(INTERFACE_SETTING)
|
|
|
|
# Port resolution:
|
|
# 1. If 'agent_tools/port' is set in project.godot, honor it strictly (fail if busy).
|
|
# 2. Otherwise try DEFAULT_PORT..PORT_RANGE_END and use the first free one.
|
|
# This lets multiple Godot editors coexist: first-to-start takes 9920, second takes 9921, etc.
|
|
var forced_port := -1
|
|
if ProjectSettings.has_setting(PORT_SETTING):
|
|
forced_port = int(ProjectSettings.get_setting(PORT_SETTING))
|
|
|
|
if forced_port != -1:
|
|
if _server.start(forced_port, forced_interface):
|
|
_bound_port = forced_port
|
|
else:
|
|
push_error("[agent_tools] configured port %s:%d is busy. Either close the other process or change 'agent_tools/port' in project.godot." % [forced_interface, forced_port])
|
|
else:
|
|
for p in range(DEFAULT_PORT, PORT_RANGE_END + 1):
|
|
if _server.start(p, forced_interface):
|
|
_bound_port = p
|
|
break
|
|
if _bound_port == -1:
|
|
push_error("[agent_tools] all ports %d-%d are busy on %s. Close other Godot editors running agent_tools, or set 'agent_tools/port' in project.godot." % [DEFAULT_PORT, PORT_RANGE_END, forced_interface])
|
|
|
|
if _bound_port != -1:
|
|
print("[agent_tools] listening on %s:%d" % [forced_interface, _bound_port])
|
|
print("[agent_tools] If you haven't already, configure your MCP client (Claude Code, Cursor, Cline, etc.) to use these tools.")
|
|
print("[agent_tools] Setup guide: https://github.com/BlakeBukowsky/GodotTools#configure-your-agent")
|
|
_write_session_registry()
|
|
|
|
# Register the runtime bridge autoload so editor.game_screenshot / logs.read
|
|
# can reach the running game. add_autoload_singleton() writes an entry to
|
|
# project.godot; remove_autoload_singleton() in _exit_tree cleans it up.
|
|
if not ProjectSettings.has_setting("autoload/" + BRIDGE_AUTOLOAD_NAME):
|
|
add_autoload_singleton(BRIDGE_AUTOLOAD_NAME, BRIDGE_AUTOLOAD_PATH)
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
_remove_session_registry()
|
|
if _server:
|
|
_server.stop()
|
|
_server.queue_free()
|
|
_server = null
|
|
_registry = null
|
|
_bound_port = -1
|
|
if ProjectSettings.has_setting("autoload/" + BRIDGE_AUTOLOAD_NAME):
|
|
remove_autoload_singleton(BRIDGE_AUTOLOAD_NAME)
|
|
|
|
|
|
# Session registry is a per-PID JSON file under a cross-project, user-scoped
|
|
# directory. The MCP shim reads these on startup so `session.list` can discover
|
|
# every running Godot editor + which port to talk to each.
|
|
func _write_session_registry() -> void:
|
|
var dir_path := _session_registry_dir()
|
|
if dir_path == "":
|
|
return
|
|
if not DirAccess.dir_exists_absolute(dir_path):
|
|
var derr := DirAccess.make_dir_recursive_absolute(dir_path)
|
|
if derr != OK:
|
|
push_warning("[agent_tools] could not create session registry dir: %s" % dir_path)
|
|
return
|
|
var pid := OS.get_process_id()
|
|
_session_file_path = dir_path.path_join("%d.json" % pid)
|
|
|
|
var project_name := ""
|
|
if ProjectSettings.has_setting("application/config/name"):
|
|
project_name = str(ProjectSettings.get_setting("application/config/name"))
|
|
var version := Engine.get_version_info()
|
|
|
|
var entry := {
|
|
"pid": pid,
|
|
"port": _bound_port,
|
|
"project_path": ProjectSettings.globalize_path("res://"),
|
|
"project_name": project_name,
|
|
"godot_version": "%d.%d.%d-%s" % [version.major, version.minor, version.patch, version.status],
|
|
"started_at_unix": int(Time.get_unix_time_from_system()),
|
|
}
|
|
var f := FileAccess.open(_session_file_path, FileAccess.WRITE)
|
|
if f == null:
|
|
push_warning("[agent_tools] could not write session file: %s" % _session_file_path)
|
|
return
|
|
f.store_string(JSON.stringify(entry, " "))
|
|
f.close()
|
|
|
|
|
|
func _remove_session_registry() -> void:
|
|
if _session_file_path == "":
|
|
return
|
|
if FileAccess.file_exists(_session_file_path):
|
|
@warning_ignore("return_value_discarded")
|
|
DirAccess.remove_absolute(_session_file_path)
|
|
_session_file_path = ""
|
|
|
|
|
|
# Cross-project location: <user home>/.godot-agent-tools/sessions/. Chosen to
|
|
# sit outside any single project's res:// or user:// tree.
|
|
func _session_registry_dir() -> String:
|
|
var home := OS.get_environment("USERPROFILE")
|
|
if home == "":
|
|
home = OS.get_environment("HOME")
|
|
if home == "":
|
|
return ""
|
|
return home.path_join(".godot-agent-tools").path_join("sessions")
|