214 lines
9.6 KiB
GDScript
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)
|