152 lines
5.6 KiB
GDScript
Vendored
152 lines
5.6 KiB
GDScript
Vendored
@tool
|
|
class_name McpClientRegistry
|
|
extends RefCounted
|
|
|
|
## Central enumeration of every supported MCP client. Adding a new client
|
|
## means: drop a file in clients/, then append one path below.
|
|
##
|
|
## Paths, not preloads (#736): a preload array pulled all client descriptor
|
|
## scripts into the boot-time compile closure of everything that preloads
|
|
## this registry (plugin.gd via client_configurator.gd and mcp_dock.gd),
|
|
## stalling "Initializing plugins" on every editor boot. Descriptors are
|
|
## only needed when the dock refreshes client statuses or a client_*
|
|
## command runs, so they load lazily on first registry access.
|
|
|
|
const _CLIENT_SCRIPT_PATHS := [
|
|
"res://addons/godot_ai/clients/claude_code.gd",
|
|
"res://addons/godot_ai/clients/claude_desktop.gd",
|
|
"res://addons/godot_ai/clients/codex.gd",
|
|
"res://addons/godot_ai/clients/grok.gd",
|
|
"res://addons/godot_ai/clients/antigravity.gd",
|
|
"res://addons/godot_ai/clients/cursor.gd",
|
|
"res://addons/godot_ai/clients/windsurf.gd",
|
|
"res://addons/godot_ai/clients/vscode.gd",
|
|
"res://addons/godot_ai/clients/vscode_insiders.gd",
|
|
"res://addons/godot_ai/clients/zed.gd",
|
|
"res://addons/godot_ai/clients/gemini_cli.gd",
|
|
"res://addons/godot_ai/clients/cline.gd",
|
|
"res://addons/godot_ai/clients/kilo_code.gd",
|
|
"res://addons/godot_ai/clients/roo_code.gd",
|
|
"res://addons/godot_ai/clients/zoo_code.gd",
|
|
"res://addons/godot_ai/clients/kiro.gd",
|
|
"res://addons/godot_ai/clients/trae.gd",
|
|
"res://addons/godot_ai/clients/cherry_studio.gd",
|
|
"res://addons/godot_ai/clients/opencode.gd",
|
|
"res://addons/godot_ai/clients/qwen_code.gd",
|
|
"res://addons/godot_ai/clients/kimi_code.gd",
|
|
"res://addons/godot_ai/clients/hermes.gd",
|
|
]
|
|
|
|
static var _instances: Array[McpClient] = []
|
|
static var _by_id: Dictionary = {}
|
|
## First registry access can come from the dock's client-status refresh
|
|
## worker thread while the main thread hits it via a client_* command —
|
|
## serialize the one-time load so a racing thread can never observe a
|
|
## half-built registry. load() itself is thread-safe via ResourceLoader.
|
|
static var _load_mutex := Mutex.new()
|
|
## True when even a fresh rebuild yields instances missing base-schema
|
|
## fields — the deep stale-script state after an in-session self-update
|
|
## (#850; docs/releasing.md release-shape rules). Only an editor restart
|
|
## heals it; callers surface RESTART_TO_FINISH_UPDATE instead of erroring
|
|
## per client. Never reset within a session: rebuilding again cannot help,
|
|
## it would only repeat the load work and warning on every dock sweep.
|
|
static var _stale_session := false
|
|
|
|
const RESTART_TO_FINISH_UPDATE := (
|
|
"Godot AI was updated in this editor session. Restart the editor to finish the update."
|
|
)
|
|
|
|
|
|
static func all() -> Array[McpClient]:
|
|
_ensure_loaded()
|
|
return _instances
|
|
|
|
|
|
static func get_by_id(id: String) -> McpClient:
|
|
_ensure_loaded()
|
|
return _by_id.get(id, null)
|
|
|
|
|
|
static func ids() -> PackedStringArray:
|
|
var out := PackedStringArray()
|
|
for c in all():
|
|
out.append(c.id)
|
|
return out
|
|
|
|
|
|
static func has_id(id: String) -> bool:
|
|
_ensure_loaded()
|
|
return _by_id.has(id)
|
|
|
|
|
|
## True when this editor session is running a self-update whose script
|
|
## reloads left descriptor state unusable. Client operations short-circuit
|
|
## with RESTART_TO_FINISH_UPDATE rather than spamming per-field errors.
|
|
static func stale_session_detected() -> bool:
|
|
_ensure_loaded()
|
|
return _stale_session
|
|
|
|
|
|
## An instance is coherent when fields added to the CURRENT McpClient schema
|
|
## read back with their declared types. After an in-session self-update,
|
|
## hot-patched or pre-update instances answer Nil for vars the update added
|
|
## (#850: `config_path_candidates` and
|
|
## `config_file_env` read as Nil, crashing platform_key / String()). The
|
|
## reflected `get()` avoids typed-access errors on such instances.
|
|
static func _instance_is_coherent(inst: Object) -> bool:
|
|
if inst == null:
|
|
return false
|
|
return (
|
|
inst.get("config_path_candidates") is Dictionary
|
|
and inst.get("config_file_env") is String
|
|
and inst.get("path_template") is Dictionary
|
|
)
|
|
|
|
|
|
static func _cache_is_coherent() -> bool:
|
|
return not _instances.is_empty() and _instance_is_coherent(_instances[0])
|
|
|
|
|
|
static func _ensure_loaded() -> void:
|
|
if _stale_session:
|
|
return
|
|
if _cache_is_coherent():
|
|
return
|
|
_load_mutex.lock()
|
|
## Re-check under the lock: another thread may have rebuilt (or concluded
|
|
## staleness) while this one waited.
|
|
if not _stale_session and not _cache_is_coherent():
|
|
## Covers both the first load and the post-self-update rebuild: this
|
|
## registry file can survive an update unchanged, so its statics keep
|
|
## serving pre-update instances to freshly reloaded callers. A rebuild
|
|
## instantiates from the reloaded descriptor scripts, which repairs
|
|
## every case except a stale base Script object itself.
|
|
_load()
|
|
if not _instances.is_empty() and not _cache_is_coherent():
|
|
_stale_session = true
|
|
push_warning("MCP | %s" % RESTART_TO_FINISH_UPDATE)
|
|
_load_mutex.unlock()
|
|
|
|
|
|
static func _load() -> void:
|
|
## Build into locals and publish whole containers last, so the lock-free
|
|
## fast path in _ensure_loaded can never see a partially-filled registry.
|
|
var instances: Array[McpClient] = []
|
|
var by_id: Dictionary = {}
|
|
for path in _CLIENT_SCRIPT_PATHS:
|
|
var script := load(path) as GDScript
|
|
if script == null:
|
|
push_warning("MCP | failed to load client descriptor %s" % path)
|
|
continue
|
|
var inst: McpClient = script.new()
|
|
if inst.id.is_empty():
|
|
push_warning("MCP | client descriptor %s has empty id" % path)
|
|
continue
|
|
if by_id.has(inst.id):
|
|
push_warning("MCP | duplicate client id: %s" % inst.id)
|
|
continue
|
|
instances.append(inst)
|
|
by_id[inst.id] = inst
|
|
_by_id = by_id
|
|
_instances = instances
|