Skip to content

Commit f45282c

Browse files
a-johnstondsarnoclaude
authored
Followup changes for #446 (#453)
* Followup changes for #446 * Review nits: docstring/comment polish from PR #453 feedback - mcp_dock.gd: clarify the Vector2i wrap with a comment explaining that `Vector2i * float` returns `Vector2`, and use `Vector2(560, 460)` as the literal so the cast reads as intentional instead of doubled. - telemetry.gd: normalize tab indentation and "MCP dock" casing in the opt-out priority docstring. - test_telemetry_setting.gd: remove the contradictory "may or may not remove the key depending on Godot version" caveat; passing null to EditorSettings.set_setting has been the documented unset path for many years. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Sarno <david@lighthaus.us> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c24ab49 commit f45282c

18 files changed

Lines changed: 486 additions & 154 deletions

docs/TELEMETRY.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ goes, and how to opt out. All telemetry code is open source and lives in
1818
- **Non-blocking**: events go through a bounded in-process queue and a
1919
single daemon worker. Telemetry failures never propagate to tool
2020
callers.
21-
- **Easy opt-out**: one environment variable. See "Opting out" below.
21+
- **Easy opt-out**: Respects opt-out via environment variable or through
22+
in-editor settings menu. See "Opting out" below.
2223

2324
## What we collect
2425

@@ -83,7 +84,7 @@ export GODOT_AI_DISABLE_TELEMETRY=true
8384
export DISABLE_TELEMETRY=true
8485
```
8586

86-
If either of the above environment variables are enabled, the opt-out is
87+
If either of the above environment variables is enabled, the opt-out is
8788
saved to Godot's editor settings and will persist between runs. Similarly,
8889
if an environment variable is explicitly set and disabled, that will be
8990
persisted to the editor settings.
@@ -93,12 +94,11 @@ startup.
9394

9495
### Effect
9596

96-
On opt-out (whether via UI or env var): the collector enters disabled
97-
mode. No records are enqueued, no UUID is generated, no worker thread is
98-
spawned, and no data directory is created. Existing local telemetry
99-
files (`customer_uuid.txt`, `milestones.json`) are deleted on the next
100-
server startup. The plugin-side helper honors the same variables and
101-
stops buffering events. Opt-out is fully side-effect-free.
97+
On opt-out, the collector enters disabled mode. No records are enqueued,
98+
no UUID is generated, no worker thread is spawned, and no data directory
99+
is created. Existing local telemetry files (`customer_uuid.txt`,
100+
`milestones.json`) are deleted on the next server startup. The plugin-side
101+
helper honors the same variables and stops buffering events.
102102

103103
## Endpoint configuration
104104

plugin/addons/godot_ai/client_configurator.gd

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,16 @@ const SERVER_NAME := "godot-ai"
3434
## the Windows-reservation diagnostics this is the escape hatch for.
3535
const DEFAULT_HTTP_PORT := 8000
3636
const DEFAULT_WS_PORT := 9500
37-
const SETTING_HTTP_PORT := "godot_ai/http_port"
38-
const SETTING_WS_PORT := "godot_ai/ws_port"
39-
const SETTING_STARTUP_TRACE := "godot_ai/log_startup_timing"
4037
const STARTUP_TRACE_ENV := "GODOT_AI_STARTUP_TRACE"
4138
const MIN_PORT := 1024
4239
const MAX_PORT := 65535
43-
44-
## Comma-separated list of tool domains to drop from the server at spawn
45-
## time. Maps 1:1 onto the `--exclude-domains` CLI flag. Set via the dock's
46-
## "Tools" tab; a change requires a server restart (the dock handles this
47-
## by triggering a plugin reload). Unknown names are warned about on the
48-
## Python side and skipped, so an EditorSetting left over from a previous
49-
## plugin version can't wedge the spawn.
50-
const SETTING_EXCLUDED_DOMAINS := "godot_ai/excluded_domains"
40+
const SETTING_WS_PORT := "godot_ai/ws_port"
41+
const SETTING_STARTUP_TRACE := "godot_ai/log_startup_timing"
5142

5243

5344
## Active HTTP port: user override (if in range) or `DEFAULT_HTTP_PORT`.
5445
static func http_port() -> int:
55-
return _read_port_setting(SETTING_HTTP_PORT, DEFAULT_HTTP_PORT)
46+
return _read_port_setting(McpSettings.SETTING_HTTP_PORT, DEFAULT_HTTP_PORT)
5647

5748

5849
## Active WebSocket port: user override (if in range) or `DEFAULT_WS_PORT`.
@@ -83,7 +74,7 @@ static func ensure_settings_registered() -> void:
8374
var es := EditorInterface.get_editor_settings()
8475
if es == null:
8576
return
86-
_register_port_setting(es, SETTING_HTTP_PORT, DEFAULT_HTTP_PORT)
77+
_register_port_setting(es, McpSettings.SETTING_HTTP_PORT, DEFAULT_HTTP_PORT)
8778
_register_port_setting(es, SETTING_WS_PORT, DEFAULT_WS_PORT)
8879
_register_bool_setting(es, SETTING_STARTUP_TRACE, false)
8980

@@ -128,9 +119,9 @@ static func startup_trace_enabled() -> bool:
128119
## `--exclude-domains` don't see an empty argument.
129120
static func excluded_domains() -> String:
130121
var es := EditorInterface.get_editor_settings()
131-
if es == null or not es.has_setting(SETTING_EXCLUDED_DOMAINS):
122+
if es == null or not es.has_setting(McpSettings.SETTING_EXCLUDED_DOMAINS):
132123
return ""
133-
var raw := str(es.get_setting(SETTING_EXCLUDED_DOMAINS))
124+
var raw := str(es.get_setting(McpSettings.SETTING_EXCLUDED_DOMAINS))
134125
var parts := PackedStringArray()
135126
for p in raw.split(","):
136127
var t := p.strip_edges()

plugin/addons/godot_ai/mcp_dock.gd

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -381,12 +381,12 @@ func _on_redock() -> void:
381381

382382

383383
func _build_margin_container(margin: int = 12) -> MarginContainer:
384-
var marginContainer := MarginContainer.new()
385-
marginContainer.add_theme_constant_override("margin_left", margin)
386-
marginContainer.add_theme_constant_override("margin_right", margin)
387-
marginContainer.add_theme_constant_override("margin_top", margin)
388-
marginContainer.add_theme_constant_override("margin_bottom", margin)
389-
return marginContainer
384+
var margin_container := MarginContainer.new()
385+
margin_container.add_theme_constant_override("margin_left", margin)
386+
margin_container.add_theme_constant_override("margin_right", margin)
387+
margin_container.add_theme_constant_override("margin_top", margin)
388+
margin_container.add_theme_constant_override("margin_bottom", margin)
389+
return margin_container
390390

391391

392392
func _build_ui() -> void:
@@ -571,7 +571,7 @@ func _build_ui() -> void:
571571

572572
var clients_open_btn := Button.new()
573573
clients_open_btn.text = "Clients & Settings"
574-
clients_open_btn.tooltip_text = "Open the MCP settings window — configure AI clients or disable tool domains to fit under a client's hard tool-count cap (e.g. Antigravity's 100)."
574+
clients_open_btn.tooltip_text = "Open the MCP settings window — configure AI clients, choose telemetry preferences, or disable tool domains to fit under a client's hard tool-count cap (e.g. Antigravity's 100)."
575575
clients_open_btn.pressed.connect(_on_open_clients_window)
576576
clients_row.add_child(clients_open_btn)
577577

@@ -595,7 +595,8 @@ func _build_ui() -> void:
595595

596596
_clients_window = Window.new()
597597
_clients_window.title = "MCP Clients & Settings"
598-
_clients_window.min_size = Vector2i(560, 460) * EditorInterface.get_editor_scale()
598+
## `Vector2i * float` yields Vector2; wrap the result back to Vector2i.
599+
_clients_window.min_size = Vector2i(Vector2(560, 460) * EditorInterface.get_editor_scale())
599600
_clients_window.visible = false
600601
_clients_window.close_requested.connect(_on_clients_window_close_requested)
601602
add_child(_clients_window)
@@ -991,7 +992,7 @@ func _on_log_logging_enabled_changed(enabled: bool) -> void:
991992
func _on_port_apply_requested(new_port: int) -> void:
992993
var es := EditorInterface.get_editor_settings()
993994
if es != null:
994-
es.set_setting(ClientConfigurator.SETTING_HTTP_PORT, new_port)
995+
es.set_setting(McpSettings.SETTING_HTTP_PORT, new_port)
995996
## Every saved client config now points at the old port. Re-sweep so the
996997
## drift banner appears in the same frame the user committed the change —
997998
## the plugin reload below will run a second sweep on its own first paint,
@@ -1015,19 +1016,13 @@ func _refresh_server_label() -> void:
10151016
# --- Telemetry setting persistence ---
10161017

10171018

1018-
func _env_truthy(var_name: String) -> bool:
1019-
var val: String = OS.get_environment(var_name).strip_edges().to_lower()
1020-
return val in ["1", "true", "yes", "on"]
1021-
1022-
10231019
## Returns true if GODOT_AI_DISABLE_TELEMETRY or DISABLE_TELEMETRY is set
10241020
## to a truthy value, false if either is set and non-truthy, null if neither
10251021
## env var is present at all.
10261022
func _is_telemetry_disabled_via_env() -> Variant:
1027-
var disabled: bool = _env_truthy("GODOT_AI_DISABLE_TELEMETRY") or _env_truthy("DISABLE_TELEMETRY")
1028-
if OS.has_environment("GODOT_AI_DISABLE_TELEMETRY") or OS.has_environment("DISABLE_TELEMETRY"):
1029-
return disabled
1030-
return null
1023+
if not (OS.has_environment("GODOT_AI_DISABLE_TELEMETRY") or OS.has_environment("DISABLE_TELEMETRY")):
1024+
return null
1025+
return McpSettings.env_truthy("GODOT_AI_DISABLE_TELEMETRY") or McpSettings.env_truthy("DISABLE_TELEMETRY")
10311026

10321027

10331028
## Reads the telemetry preference, applying env-var override when present.
@@ -1044,15 +1039,15 @@ func _load_telemetry_setting() -> void:
10441039
## the env var honour the last-set value.
10451040
enabled = not bool(env_disabled)
10461041
if es != null:
1047-
es.set_setting("godot_ai/telemetry_enabled", enabled)
1042+
es.set_setting(McpSettings.SETTING_TELEMETRY_ENABLED, enabled)
10481043
else:
10491044
## No env var: read (or create) the EditorSettings key.
1050-
if es != null and es.has_setting("godot_ai/telemetry_enabled"):
1051-
enabled = bool(es.get_setting("godot_ai/telemetry_enabled"))
1045+
if es != null and es.has_setting(McpSettings.SETTING_TELEMETRY_ENABLED):
1046+
enabled = bool(es.get_setting(McpSettings.SETTING_TELEMETRY_ENABLED))
10521047
else:
10531048
enabled = true
10541049
if es != null:
1055-
es.set_setting("godot_ai/telemetry_enabled", true)
1050+
es.set_setting(McpSettings.SETTING_TELEMETRY_ENABLED, true)
10561051

10571052
_telemetry_pending_enabled = enabled
10581053
_telemetry_saved_enabled = enabled
@@ -1117,7 +1112,7 @@ func _apply_dev_mode_visibility() -> void:
11171112
# --- Button handlers ---
11181113

11191114

1120-
func _do_plugin_reload():
1115+
func _do_plugin_reload() -> void:
11211116
EditorInterface.set_plugin_enabled("res://addons/godot_ai/plugin.cfg", false)
11221117
EditorInterface.set_plugin_enabled("res://addons/godot_ai/plugin.cfg", true)
11231118

@@ -1920,14 +1915,14 @@ func _on_tools_apply() -> void:
19201915
var canonical_excluded := ToolCatalog.canonical(_tools_pending_excluded)
19211916
var es := EditorInterface.get_editor_settings()
19221917
if es != null:
1923-
es.set_setting(ClientConfigurator.SETTING_EXCLUDED_DOMAINS, canonical_excluded)
1924-
es.set_setting("godot_ai/telemetry_enabled", _telemetry_pending_enabled)
1918+
es.set_setting(McpSettings.SETTING_EXCLUDED_DOMAINS, canonical_excluded)
1919+
es.set_setting(McpSettings.SETTING_TELEMETRY_ENABLED, _telemetry_pending_enabled)
19251920
_tools_saved_excluded = _tools_pending_excluded.duplicate()
19261921
_telemetry_saved_enabled = _telemetry_pending_enabled
19271922
_refresh_tools_ui_state()
1928-
## Plugin reload respawns the server with the new `--exclude-domains`
1929-
## flag (see `plugin.gd::_build_server_flags`). Mirrors the port-change
1930-
## Apply flow.
1923+
## Plugin reload respawns the server with the new `--exclude-domains` flag
1924+
## (see `plugin.gd::_build_server_flags`) and telemetry option. Mirrors the
1925+
## port-change Apply flow.
19311926
_on_reload_plugin()
19321927

19331928

plugin/addons/godot_ai/plugin.gd

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ static func _mcp_disabled_for_headless_launch() -> bool:
534534

535535

536536
static func _mcp_disabled_for_headless(args: PackedStringArray, display_name: String, allow_value: String) -> bool:
537-
if _env_truthy(allow_value):
537+
if McpSettings.truthy(allow_value):
538538
return false
539539
return _args_request_headless(args) or display_name.to_lower() == "headless"
540540

@@ -551,12 +551,6 @@ static func _args_request_headless(args: PackedStringArray) -> bool:
551551
return false
552552

553553

554-
static func _env_truthy(value: String) -> bool:
555-
match value.strip_edges().to_lower():
556-
"1", "true", "yes", "on":
557-
return true
558-
_:
559-
return false
560554

561555

562556
func _disable_plugin() -> void:
@@ -1538,7 +1532,10 @@ func start_dev_server() -> void:
15381532
var new_pp := worktree_src if prev_pythonpath.is_empty() else worktree_src + sep + prev_pythonpath
15391533
OS.set_environment("PYTHONPATH", new_pp)
15401534

1535+
var injected_telemetry: bool = _lifecycle._inject_telemetry_env()
15411536
var pid := OS.create_process(cmd, inner_args)
1537+
if injected_telemetry:
1538+
OS.unset_environment("GODOT_AI_DISABLE_TELEMETRY")
15421539

15431540
## Restore PYTHONPATH immediately — the spawned child has already
15441541
## copied the env, so the editor's own process state returns to

plugin/addons/godot_ai/telemetry.gd

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@
88
## and the bounded-queue worker stay in one place (Python), not
99
## duplicated in GDScript.
1010
##
11-
## Opt-out: honors `GODOT_AI_DISABLE_TELEMETRY` and `DISABLE_TELEMETRY`
12-
## environment variables on the GDScript side too, so events are never
13-
## buffered (let alone sent) when the operator opts out — useful when
14-
## a tester wants to confirm the disable flag is effective before any
15-
## handshake has happened.
11+
## Opt-out options priority:
12+
## 1. `GODOT_AI_DISABLE_TELEMETRY` / `DISABLE_TELEMETRY` env vars —
13+
## checked first so CI / operators can force-disable without touching
14+
## EditorSettings.
15+
## 2. The `godot_ai/telemetry_enabled` EditorSetting — set through the
16+
## MCP dock and persisted between sessions.
17+
##
18+
## When telemetry is disabled, events are never buffered or sent. If an env
19+
## var is explicitly set to a non-truthy value, telemetry is enabled even if
20+
## the editor setting is false.
1621
##
1722
## Buffering: events recorded before the WebSocket is connected go into
1823
## a small bounded buffer and flush on the next `record_event` call once
@@ -83,7 +88,7 @@ var _pending: Array = [] # of {name: String, data: Dictionary}
8388

8489
func _init(connection) -> void:
8590
_connection = connection
86-
_disabled = _resolve_disabled()
91+
_disabled = not McpSettings.telemetry_enabled()
8792
## Subscribe to ``connection_state_changed`` so events buffered before
8893
## the WebSocket handshake (e.g. ``record_dock_startup`` from
8994
## ``plugin._enter_tree``) actually leave the editor. Without this,
@@ -93,17 +98,6 @@ func _init(connection) -> void:
9398
if _connection != null and _connection.has_signal("connection_state_changed"):
9499
_connection.connection_state_changed.connect(_on_connection_state_changed)
95100

96-
static func _truthy(value: String) -> bool:
97-
## Match ``plugin.gd::_env_truthy`` semantics — ``strip_edges()`` so
98-
## a stray ``" true "`` from shell quoting still parses as truthy.
99-
return value.strip_edges().to_lower() in ["1", "true", "yes", "on"]
100-
101-
static func _resolve_disabled() -> bool:
102-
if _truthy(OS.get_environment("GODOT_AI_DISABLE_TELEMETRY")):
103-
return true
104-
if _truthy(OS.get_environment("DISABLE_TELEMETRY")):
105-
return true
106-
return false
107101

108102
func record_event(name: String, data: Dictionary = {}) -> void:
109103
if _disabled:

plugin/addons/godot_ai/utils/server_lifecycle.gd

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,12 @@ static func _live_package_path_for_message(live: Dictionary) -> String:
391391
## "godot_ai/telemetry_enabled" is set to false. Returns true if the var was
392392
## injected so the caller can unset it after spawning.
393393
func _inject_telemetry_env() -> bool:
394+
## Guard: if the user or CI already set an env var (even to "false"), leave
395+
## it alone. McpSettings.telemetry_enabled() only reads the EditorSetting
396+
## when no env var overrides are present.
394397
if OS.has_environment("GODOT_AI_DISABLE_TELEMETRY") or OS.has_environment("DISABLE_TELEMETRY"):
395398
return false
396-
var es := EditorInterface.get_editor_settings()
397-
var telemetry_enabled: bool = (
398-
bool(es.get_setting("godot_ai/telemetry_enabled"))
399-
if es != null and es.has_setting("godot_ai/telemetry_enabled")
400-
else true
401-
)
402-
if not telemetry_enabled:
399+
if not McpSettings.telemetry_enabled():
403400
OS.set_environment("GODOT_AI_DISABLE_TELEMETRY", "true")
404401
return true
405402
return false
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@tool
2+
class_name McpSettings
3+
extends RefCounted
4+
5+
## Shared EditorSettings key constants for the godot_ai/* namespace.
6+
##
7+
## Centralised here so lightweight files (e.g. telemetry.gd) can reference
8+
## settings keys without pulling in the full client_configurator.gd dep tree.
9+
## All keys must keep their raw string values stable across releases because
10+
## they are persisted in the user's editor_settings-4.tres.
11+
12+
const SETTING_HTTP_PORT := "godot_ai/http_port"
13+
## Comma-separated list of tool domains excluded from the server at spawn time.
14+
const SETTING_EXCLUDED_DOMAINS := "godot_ai/excluded_domains"
15+
const SETTING_TELEMETRY_ENABLED := "godot_ai/telemetry_enabled"
16+
17+
18+
## Returns true if the string value is truthy
19+
## ("1", "true", "yes", "on", case-insensitive, whitespace-trimmed).
20+
static func truthy(value: String) -> bool:
21+
return value.strip_edges().to_lower() in ["1", "true", "yes", "on"]
22+
23+
24+
## Returns true if the named environment variable is set to a truthy value.
25+
static func env_truthy(var_name: String) -> bool:
26+
return truthy(OS.get_environment(var_name))
27+
28+
29+
## Returns true if telemetry should be active, checking in priority order:
30+
## 1. GODOT_AI_DISABLE_TELEMETRY / DISABLE_TELEMETRY env vars
31+
## 2. The godot_ai/telemetry_enabled EditorSetting written by the dock UI
32+
## Defaults to true when neither source has set a preference.
33+
static func telemetry_enabled() -> bool:
34+
if env_truthy("GODOT_AI_DISABLE_TELEMETRY") or env_truthy("DISABLE_TELEMETRY"):
35+
return false
36+
var es := EditorInterface.get_editor_settings()
37+
if es != null and es.has_setting(SETTING_TELEMETRY_ENABLED):
38+
return bool(es.get_setting(SETTING_TELEMETRY_ENABLED))
39+
return true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://pefrtofs7ijw

0 commit comments

Comments
 (0)