Back to Mcpproxy Go

Anonymous Telemetry

docs/features/telemetry.md

0.47.012.3 KB
Original Source

Anonymous Telemetry

MCPProxy collects anonymous usage statistics to help improve the product. This page explains what is collected, what is not, and how to disable it.

What is collected

MCPProxy sends a daily heartbeat containing only aggregate, non-identifying information. The current schema is version 7 (schema_version: 7 in the JSON payload); the schema is forward-compatible so older consumers simply ignore fields they don't recognize.

FieldExamplePurpose
anonymous_id550e8400-...Random UUID for deduplication (not linked to you)
machine_id9f86d081... (64-hex)Stable, non-reversible salted hash of the OS machine id — dedups ephemeral installs whose anonymous_id churns every run (schema v6). Empty/omitted when unreadable. Never the raw machine id
version0.21.3Track version adoption
editionpersonalUnderstand edition usage
osdarwinPlatform distribution
archarm64Architecture distribution
server_count12Understand scale of usage
connected_server_count8Connection success rates
tool_count156Tool ecosystem size
uptime_hours47Usage patterns
routing_moderetrieve_toolsFeature adoption
quarantine_enabledtrueSecurity feature adoption
feature_flags.docker_availabletrueFraction of installs with a reachable Docker daemon (schema v3)
server_protocol_counts{"stdio":3,"http":2,"sse":0,"streamable_http":1,"auto":0}Ratio of remote-HTTP vs local-stdio upstreams (schema v3)
server_docker_isolated_count2How many configured servers the runtime actually wraps in Docker isolation (schema v3)
feature_flags.docker_isolation_enabledtrueWhether global Docker isolation is turned on (schema v5). Lets us tell "isolation on, 0 matching servers" apart from "isolation off"
feature_flags.docker_cli_sourcebundledHow the docker CLI was located — fixed enum path / bundled / login_shell / absent (schema v5). The direct signal for "Docker installed but not on the spawn PATH" (issue #696). Never the path string itself
wizard_showntrueWhether the onboarding wizard ever rendered for this install (schema v7). Makes "shown but ignored" measurable
wizard_connect_stepcompleted_externalOnboarding connect-step outcome — fixed enum, widened in v7 (see below)
web_ui_opened12Lifetime count of embedded Web UI entrypoint serves (schema v7)
days_since_install14Whole-day age of the install (schema v7). A day count, never a timestamp
active_days_30d5Distinct UTC days with process activity in the trailing 30 days (schema v7). Only the count — never the per-day breakdown
previous_shutdowncleanHow the previous process instance ended — fixed enum clean / crash, absent on first run (schema v7)
last_error_codeMCPX_DOCKER_CLI_NOT_FOUNDMost recent stable MCPX_* diagnostic code (schema v7). Enum code only, never error text

The server_protocol_counts map uses a fixed enum of keys (stdio, http, sse, streamable_http, auto) — server names and URLs are never included. Unknown or misconfigured protocol values are bucketed into auto.

The docker_cli_source field is likewise a fixed enum (path, bundled, login_shell, absent); the resolved path is never transmitted.

Docker isolation failures surface in error_code_counts_24h via three stable diagnostic codes (schema v5): MCPX_DOCKER_CLI_NOT_FOUND (isolation requested but the docker binary is unresolved — issue #696), MCPX_DOCKER_EXEC_NOT_FOUND (the image lacks the interpreter the server needs, e.g. uvx missing in python:3.11), and MCPX_DOCKER_OCI_RUNTIME (OCI runtime / architecture-mismatch failures).

What is NOT collected

The following is never collected:

  • Server names, URLs, or configurations
  • Tool names or descriptions
  • API keys, tokens, or credentials
  • File paths or environment variables
  • IP addresses (stripped by our server before storage)
  • User identity, email, or account information
  • Tool call content, arguments, or responses
  • Any user-generated content
  • The raw OS machine id or any reversible hardware identifier (only the salted, non-reversible machine_id hash is sent — see below)

Anonymous ID

The anonymous ID is a random UUID (v4) generated on first run. It has no correlation to your hardware, user account, or identity. It exists solely to deduplicate heartbeats (so we don't count the same install twice in a day).

You can delete it by removing the telemetry.anonymous_id field from your config — a new random ID will be generated on next startup.

Machine ID (schema v6)

The anonymous_id above is a UUID persisted in the config file. In ephemeral environments — throwaway HOMEs, layered Docker builds, CI runners — the config (and therefore the UUID) is regenerated on every run, so a single machine can masquerade as hundreds of distinct installs. That inflates our install counts and defeats deduplication.

machine_id fixes this without collecting anything identifying:

  • It is a salted, non-reversible hashHMAC-SHA256 keyed by the OS machine id, scoped by an mcpproxy-specific application key. The raw machine id is never transmitted; only the hash leaves your machine.
  • The application-specific key means the value cannot be correlated with any other application's telemetry that hashes the same OS machine id.
  • It is stable per physical machine, so ephemeral installs collapse to one identity for counting.
  • If the OS machine id cannot be read (a container without /etc/machine-id, a permission error, or an exotic platform), the field is simply omitted — the heartbeat is never blocked, and the backend treats an absent value as "unknown".

machine_id respects the same opt-out as every other field: when telemetry is disabled (see below), the entire heartbeat — including machine_id — is never sent.

Schema v7 — activation funnel & churn fields (Spec 080)

Schema v7 adds seven purely additive signals so we can measure whether installs come back after day one — and, when they don't, whether the last session ended cleanly or crashed. Every field keeps the established privacy posture: booleans, non-negative integers, or documented fixed enums only — no timestamps, no per-server identity, no free text. The anonymity scanner (internal/telemetry/anonymity.go) enforces these shapes on the serialized payload before every send, and all fields use omitempty, so a payload with none of them set is shape-identical to a v6 payload except for schema_version.

Widened enum: wizard_connect_step

The onboarding connect-step status (a v4 field) gains a fourth value in v7:

ValueMeaning
(absent)Step never shown to this install
completedUser completed the connect step inside the wizard
completed_externalNew in v7. User dismissed the wizard with the connect step untouched, but the install was already connected (via mcpproxy connect, the ConnectModal, or manual config). Previously miscounted as skipped
skippedUser dismissed the wizard with the connect step untouched and no connection evidence existed

Guidance for consumers: this is a string enum that may widen again. Code that switches on completed / skipped must treat unknown values as "other/engaged", never as a skip or an error. Statuses recorded before v7 are never rewritten — segment analyses by schema_version.

New fields

FieldTypeWhen it is setPrivacy rationale
wizard_shownbooleantrue once the onboarding wizard has rendered at least once for this install; omitted otherwise. Together with wizard_engaged it distinguishes "shown but ignored" from "never shown"A single boolean about our own UI; carries no user data
web_ui_openednon-negative integerLifetime count of serves of the embedded Web UI entrypoint (index document). Asset and API requests never increment it; it is independent of surface_requests.webui. Coarse by design — health checkers fetching / count tooA counter of our own page serves; no URLs, sessions, or timing
days_since_installnon-negative integerWhole-day UTC age of the install, from a persisted first-install day stamp (independent of anonymous_id). 0 on install day; clamped at 0 on clock skew. Omitted when the local store isn't available (short-lived CLI commands)Only a day count is transmitted — the install timestamp itself never leaves the machine
active_days_30dnon-negative integer (1–30)Number of distinct UTC days with process activity in the trailing 30-day window. Old days age outThe per-day set is stored locally and never transmitted — counters, not timelines
previous_shutdownfixed enum clean | crashHow the previous process instance ended: clean = the graceful-shutdown path ran; crash = it didn't (SIGKILL, panic, power loss). Absent on a first-ever run — a fresh install is never reported as a crash. Stable across all heartbeats of the current instanceOne enum value about our own process lifecycle; no stack traces, no session timing
last_error_codefixed enum (MCPX_*)The most recently observed stable diagnostic code (same fixed set as diagnostics.error_code_counts_24h), persisted across restarts so a post-crash heartbeat carries the pre-crash code. Absent when no error was ever recordedOnly the enum code is stored and sent — never error messages, server names, paths, or stack traces. The scanner rejects any value outside the fixed diagnostics catalog

Why these exist: telemetry showed most installs connect successfully but never return after day one. days_since_install + active_days_30d make retention computable from a single heartbeat (no cross-heartbeat identity joins), and previous_shutdown + last_error_code let the final heartbeat before an install goes silent distinguish "crashed and never came back" from "exited cleanly and never returned".

All v7 fields ride the same opt-out as the rest of the heartbeat: when telemetry is disabled, nothing is transmitted. Local counters may still persist on disk (so re-enabling doesn't fabricate a fresh-install picture), but they never leave the machine.

You can inspect exactly what would be sent — including every v7 field — with:

bash
mcpproxy telemetry show-payload

One-time opt-out signal

When telemetry transitions from enabled to disabled (via the CLI, the config file, or the web UI / macOS app), MCPProxy sends exactly one final, anonymous beacon — an event: "telemetry_disabled" carrying only your anonymous install ID and no usage data. It lets us count how many installs opt out so we can gauge how the feature is received. The send is best-effort: if it fails, telemetry is still disabled. After it, no further telemetry is emitted.

Disabling while already disabled (or reloading a config that is already disabled) sends nothing. Setting MCPPROXY_TELEMETRY=false is treated as "never enabled" and also sends nothing.

How to disable

There are three ways to disable telemetry:

bash
mcpproxy telemetry disable

Verify with:

bash
mcpproxy telemetry status

Re-enable anytime:

bash
mcpproxy telemetry enable

2. Configuration file

Edit ~/.mcpproxy/mcp_config.json:

json
{
  "telemetry": {
    "enabled": false
  }
}

3. Environment variable

bash
export MCPPROXY_TELEMETRY=false

This overrides the config file setting and is useful for CI/CD environments or system-wide policies.

Data handling

  • Telemetry data is sent to a Cloudflare Worker over HTTPS
  • Source IP addresses are stripped before storage
  • Data is stored in Cloudflare D1 (EU region)
  • Used only for aggregate product analytics
  • No third-party analytics services receive the data

Source code

The telemetry implementation is fully open-source: