175 lines
8.0 KiB
GDScript
175 lines
8.0 KiB
GDScript
@tool
|
|
extends EditorPlugin
|
|
## EditorPlugin entry point — thin orchestrator that delegates to PluginComposer.
|
|
|
|
const Modules := preload("res://addons/godot_mcp_toolkit/core/modules.gd")
|
|
const FileGuard = Modules.FileGuard
|
|
const SettingsRegistration := preload("res://addons/godot_mcp_toolkit/core/settings_registration.gd")
|
|
const OnboardingWizard := preload("res://addons/godot_mcp_toolkit/ui/onboarding_wizard.gd")
|
|
const PluginComposer := preload("res://addons/godot_mcp_toolkit/core/plugin_composer.gd")
|
|
const DockHost := preload("res://addons/godot_mcp_toolkit/core/dock_host.gd")
|
|
const ToolMenu := preload("res://addons/godot_mcp_toolkit/core/tool_menu.gd")
|
|
const DisableCleanupCoordinator := preload("res://addons/godot_mcp_toolkit/core/disable_cleanup_coordinator.gd")
|
|
const MCPJsonEnablePrompt := preload("res://addons/godot_mcp_toolkit/ui/mcp_json_enable_prompt.gd")
|
|
const AutoloadRegistration := preload("res://addons/godot_mcp_toolkit/core/autoload_registration.gd")
|
|
|
|
# The composed collaborator graph (server, dock, export plugin, watchers, debug
|
|
# bridge, user-path monitor, playtest watcher). PluginComposer.compose() builds
|
|
# it; the orchestrator drives it and calls _handle.dispose() on exit.
|
|
var _handle = null
|
|
|
|
var _tool_menu: ToolMenu = null
|
|
var _wizard: OnboardingWizard = null
|
|
|
|
|
|
func _enter_tree() -> void:
|
|
# Brand line, not a log line: printed first so the version is stamped even if startup fails.
|
|
print("Godot MCP Toolkit v%s — by NPGameDev · npgamedev.com" % get_plugin_version())
|
|
|
|
# Lifecycle phase sequence. The "why this order" narrative lives here; the
|
|
# composer owns the internal construction order of the graph it builds.
|
|
Modules.EditorAccess.set_plugin(self)
|
|
SettingsRegistration.register_all()
|
|
|
|
# Re-assert "plugin enabled ⟹ runtime autoload registered" before the graph is
|
|
# wired. _enable_plugin() only fires on the disabled→enabled toggle, so a project
|
|
# opened already-enabled with the autoload missing (out-of-band project.godot edit,
|
|
# VCS-propagated enabled flag, template) would silently run Mode A only. Placed
|
|
# early — pre-compose() — so the heal's settings_changed emit fires with zero
|
|
# listeners (the extension watcher and the other settings_changed consumers are
|
|
# wired inside compose() below).
|
|
AutoloadRegistration.ensure_registered()
|
|
|
|
# Construct + wire the whole collaborator graph (registry, server, debug
|
|
# bridge, command registrar, extensions, export plugin, log buffer, user-path
|
|
# monitor, registry registration, playtest watcher, write flow + dialog
|
|
# presenter, dock) and register in the system-wide registry.
|
|
_handle = PluginComposer.compose(self, _on_user_path_changed)
|
|
|
|
# Tools > MCP Toolkit submenu + command palette (needs the server, dock, and
|
|
# shared UI collaborators the composer just built).
|
|
_tool_menu = ToolMenu.new(
|
|
self, _handle.server(), _handle.dock(),
|
|
_handle.write_flow(), _handle.dialog_presenter())
|
|
_tool_menu.install()
|
|
|
|
# -- Per-user EditorSettings --
|
|
_register_editor_settings()
|
|
|
|
# Warn about untested future Godot versions (but don't block).
|
|
var _engine_ver := Modules.VersionUtils.get_engine_version_pair()
|
|
if not Modules.VersionUtils.is_at_most(_engine_ver, Modules.VersionUtils.GODOT_TESTED_MAX_VERSION):
|
|
push_warning(("[MCP] Godot %s detected - latest tested version is %s. "
|
|
+ "The plugin will run normally but some features may behave unexpectedly. "
|
|
+ "Please report issues at https://github.com/NPGameDev/godot-mcp-toolkit/issues")
|
|
% [_engine_ver, Modules.VersionUtils.GODOT_TESTED_MAX_VERSION])
|
|
|
|
_wizard = OnboardingWizard.new(
|
|
self, _handle.server(), _handle.write_flow(), _handle.dialog_presenter())
|
|
call_deferred("_check_onboarding")
|
|
|
|
|
|
func _check_onboarding() -> void:
|
|
_wizard.check_and_show()
|
|
|
|
|
|
# Reveal the toolkit dock (select its tab + expand the bottom panel). The
|
|
# onboarding wizard's final step calls this; delegating through DockHost keeps the
|
|
# editor↔version dock seam in one adapter instead of leaking make_visible here.
|
|
func reveal_dock() -> void:
|
|
if _handle != null:
|
|
DockHost.reveal(self, _handle.dock(), _handle.dock_host())
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if _handle != null:
|
|
_handle.poll_playtest()
|
|
|
|
|
|
func _on_user_path_changed() -> void:
|
|
# Static consumers that can't connect to signals themselves.
|
|
# Instance consumers (feature_settings, server) connect directly
|
|
# via bind_user_path_monitor().
|
|
OnboardingWizard.migrate_flag_after_rename()
|
|
Modules.LogBuffer.reset_tail_path()
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
# Teardown symmetry — reverse order of _enter_tree's phases.
|
|
# Onboarding wizard (if still open) — teardown() frees the dialog immediately so
|
|
# it can't outlive ObjectDB's exit-time leak check.
|
|
if _wizard != null:
|
|
_wizard.teardown()
|
|
_wizard = null
|
|
|
|
# Menus + command palette.
|
|
if _tool_menu != null:
|
|
_tool_menu.uninstall()
|
|
_tool_menu = null
|
|
|
|
# Composed graph (dock, monitor, watchers, debug bridge, export plugin,
|
|
# server + registry) — disposed in reverse construction order.
|
|
if _handle != null:
|
|
_handle.dispose()
|
|
_handle = null
|
|
|
|
# Plugin reference — clear last (teardown symmetry with _enter_tree).
|
|
Modules.EditorAccess.clear_plugin()
|
|
|
|
|
|
func _enable_plugin() -> void:
|
|
# First-enable registration shares the on-load self-heal path so the two can never
|
|
# diverge — an undo-free set_setting + save that persists to project.godot, which
|
|
# is what the game reads at F5 (rationale lives in AutoloadRegistration).
|
|
AutoloadRegistration.ensure_registered()
|
|
|
|
# Offer to (re)create a missing .mcp.json — on the enable toggle only, never
|
|
# on project open. The editor adds the plugin to the tree before calling
|
|
# _enable_plugin, so the composed graph (and its write flow) already exists.
|
|
if _handle != null:
|
|
MCPJsonEnablePrompt.show_if_needed(_handle.write_flow())
|
|
|
|
|
|
func _disable_plugin() -> void:
|
|
AutoloadRegistration.unregister(self)
|
|
|
|
# Scrub the project-local mcp_toolkit/* ProjectSettings unconditionally —
|
|
# they belong to project.godot and are meaningless once the plugin is gone.
|
|
# (EditorSettings are machine-wide and need a confirm — see below.)
|
|
SettingsRegistration.unregister_all()
|
|
|
|
# Warn about an orphaned .mcp.json, then (chained off its resolution) about
|
|
# the machine-wide EditorSettings keys. The editor frees THIS plugin the
|
|
# instant this method returns (and _exit_tree runs) — before the user answers
|
|
# the prompts — so the sequence must NOT be driven by plugin-bound callbacks
|
|
# (they would fire into a freed object and silently no-op). Hand it to a
|
|
# detached coordinator that outlives the plugin and owns the dialog flow.
|
|
DisableCleanupCoordinator.new().start()
|
|
|
|
|
|
# -- EditorSettings registration (per-user, not committed to VCS) -------------
|
|
|
|
|
|
func _register_editor_settings() -> void:
|
|
var es := EditorInterface.get_editor_settings()
|
|
# [type, default, hint_string]. These live in EDITOR Settings (per-user,
|
|
# machine-wide), NOT Project Settings: the unfocused-responsive keys control a
|
|
# machine-global editor effect and are a personal battery/CPU preference, so
|
|
# they must never be committed to project.godot / VCS.
|
|
var settings := {
|
|
"mcp_toolkit/personal/dock_default_visible": [TYPE_BOOL, true, ""],
|
|
"mcp_toolkit/performance/keep_editor_responsive_unfocused": [TYPE_BOOL, true,
|
|
"Keep the editor responsive (raise its unfocused frame rate) while an MCP client is connected, so commands stay snappy when the editor is unfocused. Off uses Godot's default low-power unfocused throttle. Raises background CPU. A toggle is also in the MCP Toolkit dock."],
|
|
"mcp_toolkit/performance/unfocused_responsive_sleep_usec": [TYPE_INT, 16666,
|
|
"Unfocused process sleep in µs applied while a client is connected (lower = higher fps = snappier but more CPU). 16666 ≈ 60 fps (default); 33333 ≈ 30 fps (power-saver). Not clamped."],
|
|
}
|
|
for key in settings:
|
|
if not es.has_setting(key):
|
|
es.set_setting(key, settings[key][1])
|
|
es.set_initial_value(key, settings[key][1], false)
|
|
var info := {"name": key, "type": settings[key][0]}
|
|
if settings[key][2] != "":
|
|
info["hint"] = PROPERTY_HINT_NONE
|
|
info["hint_string"] = settings[key][2]
|
|
es.add_property_info(info)
|