136 lines
5.9 KiB
GDScript
136 lines
5.9 KiB
GDScript
@tool
|
|
class_name McpSettings
|
|
extends RefCounted
|
|
|
|
## Shared EditorSettings key constants for the godot_ai/* namespace.
|
|
##
|
|
## Centralised here so lightweight files (e.g. telemetry.gd) can reference
|
|
## settings keys without pulling in the full client_configurator.gd dep tree.
|
|
## All keys must keep their raw string values stable across releases because
|
|
## they are persisted in the user's editor_settings-4.tres.
|
|
|
|
const SETTING_HTTP_PORT := "godot_ai/http_port"
|
|
## Comma-separated list of tool domains excluded from the server at spawn time.
|
|
const SETTING_EXCLUDED_DOMAINS := "godot_ai/excluded_domains"
|
|
const SETTING_TELEMETRY_ENABLED := "godot_ai/telemetry_enabled"
|
|
## Comma-separated CIDRs / bare IPs passed to the server as `--allow-host`
|
|
## at spawn time (#507, server core #421). Empty means loopback-only.
|
|
const SETTING_ALLOW_HOSTS := "godot_ai/allow_remote_hosts"
|
|
## Whether MCP log lines echo to the Godot console (dock "Log" toggle).
|
|
## The dock's ring-buffer log panel keeps recording regardless.
|
|
const SETTING_MCP_LOGGING := "godot_ai/mcp_logging"
|
|
## Which scope CLI-configured clients register the server in. Claude Code's
|
|
## `--scope user` writes the global block of ~/.claude.json, which that client
|
|
## loads in EVERY workspace — so the server gets spawned in unrelated,
|
|
## non-Godot projects. `project` writes <project>/.mcp.json instead, so the
|
|
## entry travels with the project. Defaults to `user` to preserve the
|
|
## historical behaviour for existing installs.
|
|
const SETTING_CLIENT_SCOPE := "godot_ai/mcp_client_scope"
|
|
|
|
## Scopes accepted for SETTING_CLIENT_SCOPE. These are the values the Claude
|
|
## Code CLI's `--scope` flag takes; an unrecognised setting falls back to
|
|
## DEFAULT_CLIENT_SCOPE rather than passing junk to the CLI.
|
|
const CLIENT_SCOPES := ["user", "project", "local"]
|
|
const DEFAULT_CLIENT_SCOPE := "user"
|
|
|
|
|
|
## Returns true if the string value is truthy
|
|
## ("1", "true", "yes", "on", case-insensitive, whitespace-trimmed).
|
|
static func truthy(value: String) -> bool:
|
|
return value.strip_edges().to_lower() in ["1", "true", "yes", "on"]
|
|
|
|
|
|
## Returns true if the named environment variable is set to a truthy value.
|
|
static func env_truthy(var_name: String) -> bool:
|
|
return truthy(OS.get_environment(var_name))
|
|
|
|
|
|
## #691: EditorInterface / EditorSettings are not thread-safe, and the dock
|
|
## runs Configure / Remove on a worker Thread (mcp_dock.gd
|
|
## `_run_client_action_worker` -> McpClientConfigurator -> McpCliStrategy
|
|
## `_format_args` -> client_scope()). Same contract as
|
|
## McpClientConfigurator._editor_setting_lookup: the main thread does a live
|
|
## read and refreshes the snapshot; worker threads read the snapshot only, so
|
|
## a never-warmed value reads as null (unset) rather than touching
|
|
## EditorInterface off-thread. Warmed on the main thread by
|
|
## McpClientConfigurator.warm_env_snapshot() before each worker dispatch.
|
|
##
|
|
## Kept here rather than reusing the configurator's `_editor_setting_lookup`
|
|
## so utils/settings.gd stays free of the client_configurator.gd dep tree —
|
|
## see this file's header.
|
|
static var _client_scope_snapshot: Variant = null
|
|
static var _client_scope_mutex := Mutex.new()
|
|
|
|
|
|
static func _client_scope_setting() -> Variant:
|
|
if OS.get_thread_caller_id() == OS.get_main_thread_id():
|
|
var live: Variant = null
|
|
if Engine.is_editor_hint():
|
|
var es := EditorInterface.get_editor_settings()
|
|
if es != null and es.has_setting(SETTING_CLIENT_SCOPE):
|
|
live = es.get_setting(SETTING_CLIENT_SCOPE)
|
|
_client_scope_mutex.lock()
|
|
_client_scope_snapshot = live
|
|
_client_scope_mutex.unlock()
|
|
return live
|
|
_client_scope_mutex.lock()
|
|
var cached: Variant = _client_scope_snapshot
|
|
_client_scope_mutex.unlock()
|
|
return cached
|
|
|
|
|
|
## Main-thread pre-warm of the scope snapshot so the dock's Configure/Remove
|
|
## worker resolves `{scope}` from a value captured while EditorSettings access
|
|
## was safe. Idempotent; called from warm_env_snapshot().
|
|
static func warm_client_scope() -> void:
|
|
_client_scope_setting()
|
|
|
|
|
|
## Returns the scope CLI clients should register the MCP server in, as
|
|
## substituted into `{scope}` in a descriptor's cli_register_template /
|
|
## cli_unregister_template. Safe to call from either thread (see
|
|
## _client_scope_setting). Unrecognised values fall back to
|
|
## DEFAULT_CLIENT_SCOPE so a hand-edited editor_settings-4.tres can never make
|
|
## the plugin shell out with a bad flag.
|
|
static func client_scope() -> String:
|
|
var value: Variant = _client_scope_setting()
|
|
## String(null) is a hard error (#850) — an unset/never-warmed key must
|
|
## short-circuit before the cast.
|
|
if value == null:
|
|
return DEFAULT_CLIENT_SCOPE
|
|
var raw := String(value).strip_edges().to_lower()
|
|
if raw in CLIENT_SCOPES:
|
|
return raw
|
|
return DEFAULT_CLIENT_SCOPE
|
|
|
|
|
|
## Returns true if telemetry should be active, checking in priority order:
|
|
## 1. GODOT_AI_DISABLE_TELEMETRY / DISABLE_TELEMETRY env vars
|
|
## 2. The godot_ai/telemetry_enabled EditorSetting written by the dock UI
|
|
## Defaults to true when neither source has set a preference.
|
|
static func telemetry_enabled() -> bool:
|
|
if env_truthy("GODOT_AI_DISABLE_TELEMETRY") or env_truthy("DISABLE_TELEMETRY"):
|
|
return false
|
|
var es := EditorInterface.get_editor_settings()
|
|
if es != null and es.has_setting(SETTING_TELEMETRY_ENABLED):
|
|
return bool(es.get_setting(SETTING_TELEMETRY_ENABLED))
|
|
return true
|
|
|
|
|
|
## Returns whether MCP log lines should echo to the Godot console. Read at
|
|
## plugin startup (to apply the persisted choice to the log buffer and
|
|
## dispatcher) and by the dock's LogViewer toggle for its initial state.
|
|
## Defaults to true when the user has never touched the toggle.
|
|
static func mcp_logging_enabled() -> bool:
|
|
var es := EditorInterface.get_editor_settings()
|
|
if es != null and es.has_setting(SETTING_MCP_LOGGING):
|
|
return bool(es.get_setting(SETTING_MCP_LOGGING))
|
|
return true
|
|
|
|
|
|
## Persist the dock "Log" toggle so the choice survives editor restarts (#626).
|
|
static func set_mcp_logging_enabled(enabled: bool) -> void:
|
|
var es := EditorInterface.get_editor_settings()
|
|
if es != null:
|
|
es.set_setting(SETTING_MCP_LOGGING, enabled)
|