2026-10-09 12:49:07 +02:00

214 lines
9.6 KiB
GDScript

@tool
extends AcceptDialog
## In-editor Info / Help dialog — read-only presentation of connection state,
## plugin/engine versions, the registered-tool summary, multi-instance support,
## read-only mode, version compatibility, Companion Skills, and reference links.
##
## Lazy-created and owned by the dock. Each show_info() re-reads live server
## state and fully rebuilds the content, so a reused instance always reflects
## the current connection.
const Modules := preload("res://addons/godot_mcp_toolkit/core/modules.gd")
const MCPJsonSync = Modules.MCPJsonSync
const DockSectionCard := preload("res://addons/godot_mcp_toolkit/ui/dock/dock_section_card.gd")
const EditorPopup := preload("res://addons/godot_mcp_toolkit/ui/editor_popup.gd")
# One-time dialog chrome (title/buttons) is installed on first show; the
# content is cleared and rebuilt on every call.
var _built: bool = false
var _content_root: VBoxContainer = null
## Re-reads live state from the passed server and renders the info panel, then
## pops the dialog centered. Pass the dock's bound MCP server node.
func show_info(server: Node) -> void:
_ensure_built()
# Clear prior content immediately — remove_child (not just queue_free) so the
# rebuilt dialog's contents-minimum-size reflects only the new content.
# queue_free alone leaves the old subtree in the tree for the current frame,
# inflating popup_centered's size on every reopen (the window is grow-only).
for child in _content_root.get_children():
_content_root.remove_child(child)
child.queue_free()
# Fixed header — Connection + plugin/engine versions, always visible above
# the collapsible middle.
var connection := DockSectionCard.make_section("Connection")
# Pin the header at a fixed height — make_section returns SIZE_EXPAND_FILL,
# which would let the header expand and swallow the collapsible middle.
connection.size_flags_vertical = 0
_content_root.add_child(connection)
var conn: VBoxContainer = connection.get_meta("content")
if server != null and server.is_listening():
var port: int = server.get_bound_port()
var peers: int = server.get_authed_peer_count()
_add_info_row(conn, "Address", "127.0.0.1:%d" % port)
_add_info_row(conn, "Peers", "%d connected" % peers)
else:
_add_info_row(conn, "Address", "not listening")
if MCPJsonSync.is_read_only():
_add_info_row(conn, "Mode", "Read-only (GODOT_MCP_READ_ONLY=1)")
var plugin_ver := Modules.VersionUtils.read_plugin_version()
var vi := Engine.get_version_info()
var godot_ver := "%d.%d.%d" % [vi["major"], vi["minor"], vi["patch"]]
_add_info_row(conn, "Plugin", "v%s" % plugin_ver)
_add_info_row(conn, "Godot", godot_ver)
# Collapsible middle — each section owns its own scroll; all start collapsed
# (the fixed header already shows the primary state), so no outer scroll.
var mid := VBoxContainer.new()
mid.size_flags_vertical = Control.SIZE_EXPAND_FILL
_content_root.add_child(mid)
var tools_content := DockSectionCard.make_collapsible(mid, "Registered Tools", false)
if server != null and server.has_method("get_command_methods"):
var methods: Array = server.get_command_methods()
methods.sort()
var groups: Dictionary = {}
for method in methods:
var parts := str(method).split(".", true, 1)
var domain: String = parts[0] if parts.size() > 0 else "other"
if not groups.has(domain):
groups[domain] = []
groups[domain].append(str(method))
var domain_keys: Array = groups.keys()
domain_keys.sort()
_add_info_row(tools_content, "Total", "%d plugin-side commands" % methods.size())
_add_note_label(tools_content, (
"These are the plugin-side commands. The agent-facing tool count is "
+ "larger and lives in the MCP server (which adds LSP, discover_tools, "
+ "and extension tools)."), true)
for domain in domain_keys:
var tools: Array = groups[domain]
_add_note_label(tools_content, " %s (%d): %s" % [
str(domain).capitalize(), tools.size(),
", ".join(PackedStringArray(tools))])
else:
_add_info_row(tools_content, "Status", "server not ready")
var multi_content := DockSectionCard.make_collapsible(mid, "Multi-Instance Multiplayer", false)
_add_note_label(multi_content, (
"A: Two copies via git worktree — FULLY SUPPORTED\n"
+ " Each editor gets its own project root, registry entry, and port.\n\n"
+ "B: Built-in multi-instance run (F5 + multiple windows) — MOSTLY SUPPORTED\n"
+ " Runtime server available; editor MCP commands limited to the host.\n\n"
+ "C: Same directory, two editors — NOT SUPPORTED\n"
+ " Port collision and registry overwrite; use Pattern A instead.\n\n"
+ "See addons/godot_mcp_toolkit/docs/multi-instance.md for full details."))
var readonly_content := DockSectionCard.make_collapsible(mid, "Read-Only Mode", false)
_add_note_label(readonly_content, (
"For supervised environments (classrooms, CI, demos).\n"
+ "Set GODOT_MCP_READ_ONLY=1 in your .mcp.json env to restrict\n"
+ "the toolkit to read-only tools only. All mutating tools\n"
+ "(create, delete, write, execute) are hidden from the AI agent.\n"
+ "Remove GODOT_MCP_READ_ONLY from .mcp.json and reconnect the\n"
+ "MCP client to restore full access.\n"
+ "Read-only is applied when the MCP server launches, so changing\n"
+ "it requires reconnecting the MCP client — existing connections\n"
+ "keep their current setting until then."))
var compat_content := DockSectionCard.make_collapsible(mid, "Compatibility", false)
_add_note_label(compat_content, (
"Supports Godot 4.2 through 4.7 (untested newer versions run\n"
+ "with a startup warning). Some tools are version-gated, so\n"
+ "older versions expose fewer. The shipped guide covers\n"
+ "per-version behavior, headless mode, C# (.NET) requirements,\n"
+ "and export stripping."))
var open_compat_btn := Button.new()
open_compat_btn.text = "Open Compatibility Guide"
var compat_doc := "res://addons/godot_mcp_toolkit/docs/compatibility.md"
open_compat_btn.pressed.connect(func(): OS.shell_open(ProjectSettings.globalize_path(compat_doc)))
compat_content.add_child(open_compat_btn)
var skills_content := DockSectionCard.make_collapsible(mid, "Companion Skills", false)
_add_note_label(skills_content, (
"Companion Skills are SKILL.md 'Agent Skills' — an open standard\n"
+ "supported by Claude Code, OpenAI Codex, Cursor, and Gemini CLI.\n"
+ "The plugin bundles skills for common toolkit workflows. Copy a\n"
+ "skill folder into your client's skills directory:\n"
+ " Claude Code: .claude/skills/\n"
+ " OpenAI Codex: ~/.agents/skills/\n"
+ " Cursor: .cursor/skills/\n"
+ " Gemini CLI: .gemini/skills/ (or .agents/skills/)\n"
+ "Paths follow each client's own docs and can change over time.\n"
+ "The dock's 'Companion Skills' button opens this same folder."))
var open_skills_btn := Button.new()
open_skills_btn.text = "Open Skills Folder"
var skills_dir := "res://addons/godot_mcp_toolkit/CompanionSkills"
open_skills_btn.pressed.connect(func(): OS.shell_open(ProjectSettings.globalize_path(skills_dir)))
skills_content.add_child(open_skills_btn)
# Fixed footer — reference links, always visible below the middle regardless
# of scroll position (mirrors the dock footer).
var footer := PanelContainer.new()
footer.add_theme_stylebox_override("panel", DockSectionCard.make_section_style())
footer.size_flags_vertical = Control.SIZE_SHRINK_END
_content_root.add_child(footer)
var links_row := HBoxContainer.new()
footer.add_child(links_row)
for pair in [
["Toolkit Repo", "https://github.com/NPGameDev/godot-mcp-toolkit"],
["Issues", "https://github.com/NPGameDev/godot-mcp-toolkit/issues"],
["Troubleshooting", "https://github.com/NPGameDev/godot-mcp-toolkit/blob/main/docs/troubleshooting.md"],
["Contributing", "https://github.com/NPGameDev/godot-mcp-toolkit/blob/main/CONTRIBUTING.md"],
["Server Repo", "https://github.com/NPGameDev/godot-mcp-server"],
]:
var btn := Button.new()
btn.text = pair[0]
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var url: String = pair[1]
btn.pressed.connect(func(): OS.shell_open(url))
links_row.add_child(btn)
# Open at an explicit size and raised (see EditorPopup). The explicit size is
# load-bearing: a no-arg popup_centered on a reused dialog reuses the window's
# current size and only grows, so sizing drifts across reopens.
EditorPopup.present(self, Vector2i(520, 460))
# Installs the dialog chrome once: title, Close button, and the content
# root that show_info() repopulates on every call.
func _ensure_built() -> void:
if _built:
return
title = "MCP Toolkit — Info / Help"
ok_button_text = "Close"
exclusive = false
min_size = Vector2i(520, 460)
_content_root = VBoxContainer.new()
_content_root.size_flags_vertical = Control.SIZE_EXPAND_FILL
_content_root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
add_child(_content_root)
_built = true
# Adds a small wrapped note label. `dim` renders it at half alpha for a
# secondary aside beneath a primary line.
func _add_note_label(parent: VBoxContainer, text: String, dim := false) -> void:
var lbl := Label.new()
lbl.text = text
lbl.add_theme_font_size_override("font_size", 11)
if dim:
lbl.add_theme_color_override("font_color", Color(1, 1, 1, 0.5))
lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
parent.add_child(lbl)
func _add_info_row(parent: VBoxContainer, key: String, value: String) -> void:
var row := HBoxContainer.new()
parent.add_child(row)
var k := Label.new()
k.text = key + ":"
k.custom_minimum_size.x = 80
k.add_theme_font_size_override("font_size", 12)
row.add_child(k)
var v := Label.new()
v.text = value
v.add_theme_font_size_override("font_size", 12)
v.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(v)