first commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user