diff --git a/ROADMAP.md b/ROADMAP.md index 4e16fd922..62bf8cdc8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1036,6 +1036,6 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` | [106-security-residual-fixes](./specs/106-security-residual-fixes/) | `shipped` | 18/19 (95%) | | [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | `shipped` | 126/126 (100%) | | [108-profiles-v3](./specs/108-profiles-v3/) | `shipped` | 194/195 (99%) | -| [109-ux-navigation-consistency](./specs/109-ux-navigation-consistency/) | `shipped` | 239/240 (100%) | +| [109-ux-navigation-consistency](./specs/109-ux-navigation-consistency/) | `shipped` | 245/246 (100%) | | [110-catalog-popularity](./specs/110-catalog-popularity/) | `in-flight` | 19/23 (83%) | | [112-client-header-forwarding](./specs/112-client-header-forwarding/) | `shipped` | 38/40 (95%) | diff --git a/cmd/mcpproxy/parity_109_cli_test.go b/cmd/mcpproxy/parity_109_cli_test.go index 420e24699..4cf99be16 100644 --- a/cmd/mcpproxy/parity_109_cli_test.go +++ b/cmd/mcpproxy/parity_109_cli_test.go @@ -60,7 +60,7 @@ func p109HelpRoot() *cobra.Command { GetAttentionCommand(), GetStatusCommand(), GetDoctorCommand(), GetUpstreamCommand(), GetReviewCommand(), GetSecurityCommand(), GetToolsCommand(), GetClientCommand(), GetConnectCommand(), GetTokenCommand(), GetCatalogCommand(), GetRegistryCommand(), - GetActivityCommand(), + GetActivityCommand(), GetTelemetryCommand(), ) clioutput.SetupHelpJSON(root) return root @@ -188,6 +188,7 @@ func TestParity109CLIGroupsAreRegisteredInMain(t *testing.T) { "upstream": "GetUpstreamCommand", "review": "GetReviewCommand", "tools": "GetToolsCommand", "client": "GetClientCommand", "connect": "GetConnectCommand", "token": "GetTokenCommand", "catalog": "GetCatalogCommand", "registry": "GetRegistryCommand", "activity": "GetActivityCommand", + "telemetry": "GetTelemetryCommand", } raw, err := os.ReadFile(filepath.Join("..", "..", "specs", "109-ux-navigation-consistency", "parity-matrix.json")) require.NoError(t, err) diff --git a/docs/api/rest-api.md b/docs/api/rest-api.md index e3a83ecb9..dee42bf95 100644 --- a/docs/api/rest-api.md +++ b/docs/api/rest-api.md @@ -102,25 +102,35 @@ curl "http://127.0.0.1:8080/api/v1/activity?request_id=a1b2c3d4-e5f6-7890-abcd-e #### GET /api/v1/status -Get server status and statistics. +Get server status and statistics. The `data` object carries `running`, `edition`, `listen_addr`, `routing_mode`, `upstream_stats`, `started_at`, `timestamp` and the blocks below. (An earlier version of this page showed a different shape; it was stale.) -**Response:** +**Response (abridged):** ```json { - "status": "running", - "version": "0.11.0", - "uptime": 3600, - "servers": { - "total": 5, - "connected": 4, - "quarantined": 1 - }, - "tools": { - "total": 42 + "success": true, + "data": { + "running": true, + "edition": "personal", + "listen_addr": "127.0.0.1:8080", + "routing_mode": "retrieve_tools", + "upstream_stats": { "total_servers": 5, "connected_servers": 4, "quarantined_servers": 1, "total_tools": 42 }, + "telemetry": { "enabled": false, "source": "env", "disabled_by": "MCPPROXY_TELEMETRY=false" } } } ``` +**`telemetry`** is the effective telemetry state of the running core, so a UI can say whether telemetry is on and why. It is withheld from scoped callers (agent tokens), like `activation`. + +| Field | Description | +|-------|-------------| +| `enabled` | Whether the core sends telemetry. Always equal to the resolved state: an environment opt-out wins over the config file. | +| `source` | `env` (an environment variable disabled it), `config` (`telemetry.enabled` is set in the config file, true or false) or `default` (unset, which means on). | +| `disabled_by` | Present only when `source` is `env`: `DO_NOT_TRACK`, `CI` or `MCPPROXY_TELEMETRY=false`. | + +`GET /api/v1/config` keeps returning the stored `telemetry.enabled`, which can differ from `enabled` here when an environment variable overrides it. A dev (non-release) build never transmits whatever `enabled` says. + +While an environment variable forces telemetry off, `POST /api/v1/config/apply` and `PATCH /api/v1/config` answer `422` and write nothing if the document would change `telemetry.enabled` (the value is judged after decoding, so a miscased key is caught too). A document that leaves `telemetry.enabled` as stored is accepted. + ### Servers #### GET /api/v1/servers diff --git a/docs/development/macos-tray.md b/docs/development/macos-tray.md index bbbb73231..d6afca65f 100644 --- a/docs/development/macos-tray.md +++ b/docs/development/macos-tray.md @@ -72,7 +72,7 @@ After a change under `Views/Profiles*`, `Views/Client*`, `Views/AccessExplainer* 10. **Scoped views.** Tools "View as Client… / Profile…" greys rows the subject cannot use, with the reason and a "Why?" button that opens the explainer. Activity shows a Caller column and Profile, Client and Token pickers. Home filters usage and sessions by the same three and shows Profile and Source columns. Servers filters by profile. 11. **Width and accessibility.** At a 900 pt window nothing is clipped on Profiles, the editor and Clients; run `check_accessibility`. The accessibility ids are listed in the plan for 108-k (K22): `profile-card-`, `profile-editor`, `profile-editor-save`, `client-profile-picker-`, `client-lock-toggle-`, `clients-warnings-banner`, `access-explainer`, `guard-refusal`, `settings-anonymous-profile`, `connect-profile-picker`, `connect-lock-toggle`, `connect-mgmt-notice`. -**Driving the window without screenshots.** `screenshot_window` needs Screen Recording; when it fails (`Failed to capture window`), drive and read the window through the accessibility tree instead (`AXPress`, `AXValue`, `AXFocused` on the `profile-*`, `client-*`, `token-*`, `connect-*` ids above), keystroke into focused fields with System Events, and check a 900 pt window by listing elements whose frame leaves the window. Navigation shortcuts: ⌘1 Home, ⌘2 Clients, ⌘3 Profiles, ⌘4 Servers, ⌘5 Tools. A tray attached to an already-running core (`MCPPROXY_TRAY_SKIP_CORE=1`) needs the core's `MCPPROXY_SOCKET_PATH`; the SSE stream authenticates with the key it reads from `/api/v1/info`. +**Driving the window without screenshots.** `screenshot_window` needs Screen Recording; when it fails (`Failed to capture window`), drive and read the window through the accessibility tree instead (`AXPress`, `AXValue`, `AXFocused` on the `profile-*`, `client-*`, `token-*`, `connect-*` ids above), keystroke into focused fields with System Events, and check a 900 pt window by listing elements whose frame leaves the window. Navigation shortcuts: ⌘1 Home, ⌘2 Clients, ⌘3 Profiles, ⌘4 Servers, ⌘5 Tools. A tray attached to an already-running core (`MCPPROXY_TRAY_SKIP_CORE=1`) needs the core's `MCPPROXY_SOCKET_PATH`; the SSE stream authenticates with the key it reads from `/api/v1/info`. The tray ignores `HOME` (the Go core honours it), and an app started with plain `open` drops shell variables, so a dev bundle meant for a scratch core must be launched as the binary directly (or with `open --env …`) with `MCPPROXY_TRAY_SKIP_CORE=1 MCPPROXY_SOCKET_PATH=/mcpproxy.sock`; otherwise it attaches to the real `~/.mcpproxy` core, which is how a first-run test once reported the wrong version and telemetry state. Before trusting any observation, confirm that Settings → Security shows "Connected core vX is listening on …" naming the scratch build. **MCP config** (in Claude Code settings or `~/.claude/settings.json`): ```json diff --git a/docs/features/telemetry.md b/docs/features/telemetry.md index 6de1b86ef..99b3e336d 100644 --- a/docs/features/telemetry.md +++ b/docs/features/telemetry.md @@ -319,6 +319,12 @@ export MCPPROXY_TELEMETRY=false This overrides the config file setting and is useful for CI/CD environments or system-wide policies. +### How the UI shows an environment opt-out + +When an environment variable (`MCPPROXY_TELEMETRY=false`, `DO_NOT_TRACK` or `CI`) disables telemetry, the Web UI and the macOS app say so instead of showing the usual notice: the setup wizard's last step, the Home banner and the macOS welcome read "Anonymous usage telemetry is off — disabled by MCPPROXY_TELEMETRY=false in the environment. Nothing is sent." The telemetry toggle in Settings is shown off and disabled with the same reason; unset the variable and restart MCPProxy to change it. If you turned telemetry off in the config file yourself, no notice is shown. The core reports this state at `telemetry` in `GET /api/v1/status`. + +Development builds (a version that is not a release number) never send telemetry, whatever the setting. That is a property of the build and is not reflected in the UI. + ## Data handling - Telemetry data is sent to a Cloudflare Worker over HTTPS diff --git a/frontend/src/components/OnboardingWizard.vue b/frontend/src/components/OnboardingWizard.vue index b5eb38b10..6c0c3e4d8 100644 --- a/frontend/src/components/OnboardingWizard.vue +++ b/frontend/src/components/OnboardingWizard.vue @@ -581,21 +581,7 @@ final step. TelemetryBanner.vue hides itself while the wizard is open, so this is the only place it appears until the user closes the wizard. --> -

- - MCPProxy sends anonymous usage statistics to help improve the product. No personal data is collected. - Learn more - - -

+ @@ -705,6 +691,7 @@ import ManualServerForm from '@/components/ManualServerForm.vue' import ImportServers from '@/components/ImportServers.vue' import ClientConnectList from '@/components/ClientConnectList.vue' import ReviewQueueList from '@/components/ReviewQueueList.vue' +import TelemetryBanner from '@/components/TelemetryBanner.vue' import { useDialogOpen } from '@/composables/useDialogOpen' import { skipReasonLabel } from '@/utils/importSkipReason' import { serversStepView, awaitingReviewSentence } from '@/utils/onboardingServersStep' @@ -799,18 +786,6 @@ const loadingImportSources = ref(false) const recentActivity = ref([]) const loadingActivity = ref(false) -// Spec 109-b FR-044: the telemetry notice's one-line form for the wizard's -// final step, sharing TelemetryBanner.vue's dismissal state (the store's -// `telemetryNoticeDismissed` ref, backed by one localStorage key) so acting -// on either surface silences both immediately — the banner and this wizard -// are mounted together on Dashboard.vue for the whole session, so a -// component-local copy read only at mount would miss the other surface's -// dismissal until a full reload. -const telemetryNoticeDismissed = computed(() => onboarding.telemetryNoticeDismissed) -function dismissTelemetryNotice() { - onboarding.dismissTelemetryNotice() -} - // Verify tab — second milestone (UX audit F13). Lifetime flag from the // Spec 044 activation bucket, read off `GET /api/v1/status`, which already // serves the whole block to an admin caller. The activity log cannot answer diff --git a/frontend/src/components/TelemetryBanner.vue b/frontend/src/components/TelemetryBanner.vue index 25a1188ad..594282e80 100644 --- a/frontend/src/components/TelemetryBanner.vue +++ b/frontend/src/components/TelemetryBanner.vue @@ -1,14 +1,53 @@