Documentation
¶
Index ¶
- Constants
- Variables
- func BucketTokens(n int) string
- func DetectEnvKind(env map[string]string, fs FileProber, osName string, tty TTYChecker) (EnvKind, EnvMarkers)
- func DetectEnvKindOnce() (EnvKind, EnvMarkers)
- func DetectEnvKindOnceWith(detector func() (EnvKind, EnvMarkers)) (EnvKind, EnvMarkers)
- func EnsureActivationBucket(db *bbolt.DB) error
- func IsBuiltinTool(name string) bool
- func IsValidEnvKind(v EnvKind) bool
- func IsValidErrorCategory(c ErrorCategory) bool
- func IsValidLaunchSource(v LaunchSource) bool
- func MaybePrintFirstRunNotice(cfg *config.Config, w io.Writer) bool
- func PopulateBlockedValues()
- func RecordBuiltinToolOn(reg *CounterRegistry, name string)
- func RecordErrorOn(reg *CounterRegistry, c ErrorCategory)
- func RecordRESTRequestOn(reg *CounterRegistry, method, template, statusClass string)
- func RecordSurfaceOn(reg *CounterRegistry, s Surface)
- func RecordUpstreamToolOn(reg *CounterRegistry)
- func ResetEnvKindForTest()
- func ScanForPII(payloadJSON []byte) error
- func SortedOAuthProviderTypes(types []string) []string
- func ValidateCategory(category string) bool
- func ValidateMessage(message string) error
- type ActivationState
- type ActivationStore
- type AnonymityViolation
- type AutostartReader
- type CounterRegistry
- func (r *CounterRegistry) AnonymityViolationsTotal() int64
- func (r *CounterRegistry) RecordAnonymityViolation()
- func (r *CounterRegistry) RecordBuiltinTool(name string)
- func (r *CounterRegistry) RecordDoctorRun(results []DoctorCheckResult)
- func (r *CounterRegistry) RecordError(c ErrorCategory)
- func (r *CounterRegistry) RecordRESTRequest(method, template, statusClass string)
- func (r *CounterRegistry) RecordSurface(s Surface)
- func (r *CounterRegistry) RecordUpstreamTool()
- func (r *CounterRegistry) Reset()
- func (r *CounterRegistry) Snapshot() RegistrySnapshot
- type DoctorCheckResult
- type DoctorCounts
- type EnvDisabledReason
- type EnvKind
- type EnvMarkers
- type ErrorCategory
- type FeatureFlagSnapshot
- type FeedbackContext
- type FeedbackRequest
- type FeedbackResponse
- type FileProber
- type HandshakeChecker
- type HeartbeatPayload
- type LaunchSource
- type PPIDChecker
- type RateLimiter
- type RegistrySnapshot
- type RuntimeStats
- type Service
- func (s *Service) ActivationDB() *bbolt.DB
- func (s *Service) ActivationStore() ActivationStore
- func (s *Service) BuildPayload() HeartbeatPayload
- func (s *Service) EnvDisabledReason() EnvDisabledReason
- func (s *Service) Registry() *CounterRegistry
- func (s *Service) SetActivationStore(store ActivationStore, db *bbolt.DB)
- func (s *Service) SetAutostartReader(r *AutostartReader)
- func (s *Service) SetConfiguredIDECountProvider(fn func() int)
- func (s *Service) SetRuntimeStats(stats RuntimeStats)
- func (s *Service) Start(ctx context.Context)
- func (s *Service) SubmitFeedback(ctx context.Context, req *FeedbackRequest) (*FeedbackResponse, error)
- type Surface
- type TTYChecker
Constants ¶
const ActivationBucketName = "activation"
ActivationBucketName is the BBolt bucket that stores retention activation state (spec 044). Keys inside the bucket are fixed at compile time; missing keys default to their zero value.
const FirstRunNoticeText = `` /* 192-byte string literal not displayed */
FirstRunNoticeText is the one-time banner printed to stderr the first time `mcpproxy serve` runs against a config that has no telemetry_notice_shown flag. Spec 042 User Story 10.
const MaxMCPClientsSeen = 16
MaxMCPClientsSeen bounds the cardinality of the mcp_clients_seen_ever list. 17th insertion is dropped.
const SchemaVersion = 3
SchemaVersion is the heartbeat payload schema version. v1 payloads have no such field; receivers can route by absence vs presence.
v3 (schema bump from 2): adds feature_flags.docker_available, server_protocol_counts, and server_docker_isolated_count. Forward-compatible: existing v2 consumers simply ignore the new fields.
Variables ¶
var AnonymityBlockedPrefixes = []string{
"/Users/",
"/home/",
`C:\\Users\\`,
"/var/folders/",
}
AnonymityBlockedPrefixes is the fixed list of byte-prefix substrings that MUST NOT appear anywhere in a serialized telemetry payload. These patterns catch accidental leaks of user home directories, macOS temp folders, and Windows user profile paths.
Additional runtime-detected values (current hostname, home-dir basename, env-var values from a blocked set) are appended by the telemetry service at startup via BlockedValues — see T025 / spec 044. NOTE on backslash form: telemetry payloads are JSON, and JSON encodes a single backslash as two bytes (`\\`). So the Windows user-profile prefix appears on the wire as `C:\\Users\\`. We match the JSON-escaped form here.
var BlockedValues []string
BlockedValues is the runtime-populated list of literal substrings that MUST NOT appear in a payload. Populated at startup by the telemetry service from:
- os.Hostname() (if non-empty)
- last path component of os.UserHomeDir() (if non-empty)
- values of env vars in the blocked set (GITHUB_TOKEN, GITLAB_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY), when non-empty.
The package-level var is intentionally mutable so tests can inject fake values. Production code should call SetBlockedValues (added in T025) once at startup; after that, treat the slice as read-only.
var ErrAnonymityViolation = errors.New("telemetry anonymity violation")
ErrAnonymityViolation is a sentinel returned (wrapped) by ScanForPII when a violation is found. Tests use errors.As against *AnonymityViolation for richer detail; errors.Is against ErrAnonymityViolation remains valid for callers that only care about the category.
Functions ¶
func BucketTokens ¶ added in v0.25.0
BucketTokens maps a raw token count to the fixed bucket enum. Buckets: "0", "1_100", "100_1k", "1k_10k", "10k_100k", "100k_plus".
Boundaries (inclusive upper edge matches the bucket label):
- 0 -> "0"
- 1..100 -> "1_100"
- 101..1000 -> "100_1k"
- 1001..10000 -> "1k_10k"
- 10001..100000 -> "10k_100k"
- >100000 -> "100k_plus"
func DetectEnvKind ¶ added in v0.25.0
func DetectEnvKind(env map[string]string, fs FileProber, osName string, tty TTYChecker) (EnvKind, EnvMarkers)
DetectEnvKind is the pure classifier that decides env_kind + env_markers from an injected environment map, filesystem prober, OS name, and TTY checker. Ordering is authoritative: cloud_ide wins over CI wins over container.
NOTE: This ordering deviates from the original design doc (§4.2 / research.md R1) which put CI first. The change is motivated by gemini P1 cross-review: GitHub Codespaces and Gitpod routinely set CI=true alongside their cloud-IDE markers (CODESPACES, GITPOD_WORKSPACE_ID). Real humans in those ephemeral cloud sessions were being classified as `ci`, artificially deflating the Cloud IDE retention funnel. Prioritising cloud_ide over CI keeps interactive human sessions in the right bucket while still catching ordinary CI runners.
Inputs:
- env: map of env-var name → value (caller builds this from os.Environ).
- fs: filesystem prober for /.dockerenv + /run/.containerenv.
- osName: runtime.GOOS value ("darwin", "linux", "windows", …).
- tty: stdin-is-a-TTY checker.
Outputs:
- EnvKind: one of the canonical constants (never empty).
- EnvMarkers: booleans reflecting every observation feeding the decision, including ones that were irrelevant to the final verdict (e.g. on CI a container marker may still be true).
func DetectEnvKindOnce ¶ added in v0.25.0
func DetectEnvKindOnce() (EnvKind, EnvMarkers)
DetectEnvKindOnce runs the real detector against os.Environ / runtime.GOOS / os.Stdin exactly once per process lifetime and caches the result. Subsequent callers get the cached value. Safe for concurrent use.
func DetectEnvKindOnceWith ¶ added in v0.25.0
func DetectEnvKindOnceWith(detector func() (EnvKind, EnvMarkers)) (EnvKind, EnvMarkers)
DetectEnvKindOnceWith is the test-only entry point: it runs the given detector function at most once per reset cycle and caches the result. Production code should always use DetectEnvKindOnce. This helper exists so the concurrency test can inject a counting fake detector.
func EnsureActivationBucket ¶ added in v0.25.0
ensureBucket ensures the activation bucket exists (useful for migration). Exported so callers that wire up storage can pre-create it to avoid races on first write. Safe to call multiple times.
func IsBuiltinTool ¶ added in v0.24.0
IsBuiltinTool reports whether the given tool name is in the fixed enum.
func IsValidEnvKind ¶ added in v0.25.0
IsValidEnvKind reports whether v is one of the canonical EnvKind constants.
func IsValidErrorCategory ¶ added in v0.24.0
func IsValidErrorCategory(c ErrorCategory) bool
IsValidErrorCategory reports whether the given category is in the fixed enum.
func IsValidLaunchSource ¶ added in v0.25.0
func IsValidLaunchSource(v LaunchSource) bool
IsValidLaunchSource reports whether v is one of the canonical LaunchSource constants.
func MaybePrintFirstRunNotice ¶ added in v0.24.0
MaybePrintFirstRunNotice prints the first-run telemetry notice to w if it has not been shown before. It mutates cfg.Telemetry.NoticeShown so the caller can persist the change. Returns true if the notice was printed.
Spec 042 User Story 10. Idempotent: subsequent calls with the same config are no-ops.
func PopulateBlockedValues ¶ added in v0.25.0
func PopulateBlockedValues()
PopulateBlockedValues (Spec 044 T025) scans the current process's OS-level identity (hostname, user home dir, sensitive env vars) and appends any non-empty literal to BlockedValues. Safe to call multiple times — only the first call has effect.
Inputs are read through function pointers so tests can inject fakes without reshelling os.Hostname / os.Getenv.
func RecordBuiltinToolOn ¶ added in v0.24.0
func RecordBuiltinToolOn(reg *CounterRegistry, name string)
RecordBuiltinToolOn calls reg.RecordBuiltinTool(name) if reg is non-nil.
func RecordErrorOn ¶ added in v0.24.0
func RecordErrorOn(reg *CounterRegistry, c ErrorCategory)
RecordErrorOn calls reg.RecordError(c) if reg is non-nil.
func RecordRESTRequestOn ¶ added in v0.24.0
func RecordRESTRequestOn(reg *CounterRegistry, method, template, statusClass string)
RecordRESTRequestOn calls reg.RecordRESTRequest(...) if reg is non-nil.
func RecordSurfaceOn ¶ added in v0.24.0
func RecordSurfaceOn(reg *CounterRegistry, s Surface)
RecordSurfaceOn calls reg.RecordSurface(s) if reg is non-nil.
func RecordUpstreamToolOn ¶ added in v0.24.0
func RecordUpstreamToolOn(reg *CounterRegistry)
RecordUpstreamToolOn calls reg.RecordUpstreamTool() if reg is non-nil.
func ResetEnvKindForTest ¶ added in v0.25.0
func ResetEnvKindForTest()
ResetEnvKindForTest resets the cached env_kind result so tests can rerun DetectEnvKindOnce with fresh inputs. MUST NOT be called from production code — calling it would re-detect on config reload and potentially re-classify a CI runner that set env vars late. Guard calls behind _test.go files only.
func ScanForPII ¶ added in v0.25.0
ScanForPII scans a serialized v3 telemetry payload for PII leaks and structural violations. Returns nil when the payload is clean; otherwise returns an *AnonymityViolation. The returned error satisfies errors.Is(err, ErrAnonymityViolation).
Rules (first-match wins):
- Any substring from AnonymityBlockedPrefixes appears in the payload.
- Any substring from BlockedValues appears in the payload.
- env_markers, if present, fails to unmarshal into a strict all-bool struct — meaning a field widened to a string/number/null.
The implementation never logs the payload — it only reports which rule tripped and the offending pattern (a small literal). Callers should log at error level and skip the heartbeat.
func SortedOAuthProviderTypes ¶ added in v0.24.0
SortedOAuthProviderTypes returns a sorted, deduplicated list. Helper used by feature_flags.go but defined here to avoid an extra file.
func ValidateCategory ¶
ValidateCategory checks if the feedback category is valid.
func ValidateMessage ¶
ValidateMessage checks if the feedback message meets length requirements.
Types ¶
type ActivationState ¶ added in v0.25.0
type ActivationState struct {
FirstConnectedServerEver bool `json:"first_connected_server_ever"`
FirstMCPClientEver bool `json:"first_mcp_client_ever"`
FirstRetrieveToolsCallEver bool `json:"first_retrieve_tools_call_ever"`
MCPClientsSeenEver []string `json:"mcp_clients_seen_ever"`
RetrieveToolsCalls24h int `json:"retrieve_tools_calls_24h"`
EstimatedTokensSaved24hBucket string `json:"estimated_tokens_saved_24h_bucket"`
ConfiguredIDECount int `json:"configured_ide_count"`
}
ActivationState is the in-memory / on-the-wire representation of the activation bucket. Instances are loaded with ActivationStore.Load and saved with ActivationStore.Save.
type ActivationStore ¶ added in v0.25.0
type ActivationStore interface {
// Load reads the full activation state. Missing bucket or keys yield
// zero values.
Load(db *bbolt.DB) (ActivationState, error)
// Save writes the full activation state, enforcing monotonic flags
// (true cannot revert to false).
Save(db *bbolt.DB, st ActivationState) error
// MarkFirstConnectedServer sets first_connected_server_ever=true if not
// already set. No-op if already true.
MarkFirstConnectedServer(db *bbolt.DB) error
// MarkFirstMCPClient sets first_mcp_client_ever=true if not already set.
MarkFirstMCPClient(db *bbolt.DB) error
// MarkFirstRetrieveToolsCall sets first_retrieve_tools_call_ever=true
// if not already set.
MarkFirstRetrieveToolsCall(db *bbolt.DB) error
// RecordMCPClient adds sanitized client name to the seen list. Dedups
// on insert; drops when cap (16) is reached.
RecordMCPClient(db *bbolt.DB, name string) error
// IncrementRetrieveToolsCall bumps the 24h window counter by 1,
// rolling the window if it has expired.
IncrementRetrieveToolsCall(db *bbolt.DB) error
// AddTokensSaved adds n to the 24h token-savings estimator counter.
AddTokensSaved(db *bbolt.DB, n int) error
// SetInstallerPending writes the installer_heartbeat_pending flag.
SetInstallerPending(db *bbolt.DB, v bool) error
// IsInstallerPending reports whether installer_heartbeat_pending is
// currently set.
IsInstallerPending(db *bbolt.DB) (bool, error)
}
ActivationStore is the persistence contract for the activation bucket. Implementations back onto a BBolt database; a fake is used in tests.
All methods are safe for concurrent use by the caller's discretion — the BBolt implementation uses transactional updates so individual method calls are atomic. Callers that need multi-step atomicity should serialize through a single goroutine.
func NewActivationStore ¶ added in v0.25.0
func NewActivationStore() ActivationStore
NewActivationStore returns a BBolt-backed ActivationStore.
type AnonymityViolation ¶ added in v0.25.0
type AnonymityViolation struct {
// Rule is a short stable identifier ("blocked_prefix", "blocked_value",
// "env_markers_non_bool") suitable for metrics labels.
Rule string
// Pattern is the literal substring or env-var name that matched.
Pattern string
// Reason is a human-readable summary (no payload contents).
Reason string
}
AnonymityViolation identifies which rule the payload tripped. The Pattern field is the offending substring (never the full payload: logging the whole payload would defeat the purpose of the scan).
func (*AnonymityViolation) Error ¶ added in v0.25.0
func (v *AnonymityViolation) Error() string
Error implements the error interface.
func (*AnonymityViolation) Is ¶ added in v0.25.0
func (v *AnonymityViolation) Is(target error) bool
Is allows AnonymityViolation to match ErrAnonymityViolation via errors.Is.
type AutostartReader ¶ added in v0.25.0
type AutostartReader struct {
// Path is the filesystem location of the tray-written sidecar. Tests
// override this; production uses DefaultAutostartReader.
Path string
// contains filtered or unexported fields
}
AutostartReader reads the tray-written autostart state. Instances are safe for concurrent use and cache the result for autostartCacheTTL. A nil return value means "state unknown" — this is expected on Linux (no tray today), when the tray is not running, or when the tray sidecar is absent / malformed.
func DefaultAutostartReader ¶ added in v0.25.0
func DefaultAutostartReader() *AutostartReader
DefaultAutostartReader returns the production reader targeting ~/.mcpproxy/tray-autostart.json. On Linux the sidecar is never written (by design), so the returned reader will simply always yield nil — the heartbeat's AutostartEnabled field stays JSON-null, matching data-model.md.
func (*AutostartReader) Read ¶ added in v0.25.0
func (r *AutostartReader) Read() *bool
Read returns the current autostart state (true=enabled, false=disabled, nil=unknown). Result is cached for autostartCacheTTL after the first successful read. Errors and missing files are logged nowhere (the caller — the telemetry service — already logs pipeline failures).
type CounterRegistry ¶ added in v0.24.0
type CounterRegistry struct {
// contains filtered or unexported fields
}
CounterRegistry aggregates Tier 2 telemetry counters in memory. All methods are safe for concurrent use. Counters are zeroed only by Reset(), which the telemetry service calls after a successful heartbeat send.
func NewCounterRegistry ¶ added in v0.24.0
func NewCounterRegistry() *CounterRegistry
NewCounterRegistry creates an empty registry. All counters start at zero.
func (*CounterRegistry) AnonymityViolationsTotal ¶ added in v0.25.0
func (r *CounterRegistry) AnonymityViolationsTotal() int64
AnonymityViolationsTotal returns the lifetime count of anonymity-scanner rejections. Not transmitted; for test + internal observability only.
func (*CounterRegistry) RecordAnonymityViolation ¶ added in v0.25.0
func (r *CounterRegistry) RecordAnonymityViolation()
RecordAnonymityViolation increments the anonymity-violation counter (Spec 044). Called by the telemetry service when ScanForPII rejects a payload.
func (*CounterRegistry) RecordBuiltinTool ¶ added in v0.24.0
func (r *CounterRegistry) RecordBuiltinTool(name string)
RecordBuiltinTool increments the counter for the named built-in tool. Unknown names (i.e., upstream tool names) are silently dropped.
func (*CounterRegistry) RecordDoctorRun ¶ added in v0.24.0
func (r *CounterRegistry) RecordDoctorRun(results []DoctorCheckResult)
RecordDoctorRun aggregates the structured doctor check results into the registry's doctor counter. Each result increments either Pass or Fail for its check name.
func (*CounterRegistry) RecordError ¶ added in v0.24.0
func (r *CounterRegistry) RecordError(c ErrorCategory)
RecordError increments the counter for the given error category. Unknown categories are silently dropped.
func (*CounterRegistry) RecordRESTRequest ¶ added in v0.24.0
func (r *CounterRegistry) RecordRESTRequest(method, template, statusClass string)
RecordRESTRequest increments the counter for the given route template and status class. Both inputs are expected to be from a fixed enum (Chi route template + "2xx"/"3xx"/"4xx"/"5xx") so no sanitization is needed here.
func (*CounterRegistry) RecordSurface ¶ added in v0.24.0
func (r *CounterRegistry) RecordSurface(s Surface)
RecordSurface increments the counter for the given surface.
func (*CounterRegistry) RecordUpstreamTool ¶ added in v0.24.0
func (r *CounterRegistry) RecordUpstreamTool()
RecordUpstreamTool increments the upstream tool call counter. The tool name itself is intentionally not accepted: only an aggregate count is recorded.
func (*CounterRegistry) Reset ¶ added in v0.24.0
func (r *CounterRegistry) Reset()
Reset zeros all counters. Called only after a successful heartbeat send.
func (*CounterRegistry) Snapshot ¶ added in v0.24.0
func (r *CounterRegistry) Snapshot() RegistrySnapshot
Snapshot returns an immutable view of all counters. The registry is NOT reset; call Reset() after a successful flush.
type DoctorCheckResult ¶ added in v0.24.0
DoctorCheckResult is the minimal interface the registry needs to record a doctor check outcome. It is satisfied by internal/doctor.CheckResult without importing that package (avoiding an import cycle).
type DoctorCounts ¶ added in v0.24.0
DoctorCounts holds pass/fail counts for a single doctor check.
type EnvDisabledReason ¶ added in v0.24.0
type EnvDisabledReason string
EnvDisabledReason explains why telemetry was disabled by an environment variable, if any. The empty string means env did not disable telemetry.
const ( EnvDisabledNone EnvDisabledReason = "" EnvDisabledByDoNotTrack EnvDisabledReason = "DO_NOT_TRACK" EnvDisabledByCI EnvDisabledReason = "CI" EnvDisabledByMCPProxy EnvDisabledReason = "MCPPROXY_TELEMETRY=false" )
func IsDisabledByEnv ¶ added in v0.24.0
func IsDisabledByEnv() (bool, EnvDisabledReason)
IsDisabledByEnv evaluates the env var precedence chain for telemetry disablement. Precedence (highest first):
- DO_NOT_TRACK set to any non-empty, non-"0" value (consoledonottrack.com)
- CI=true or CI=1
- MCPPROXY_TELEMETRY=false
Returns true and the reason if telemetry should be disabled.
type EnvKind ¶ added in v0.25.0
type EnvKind string
EnvKind classifies the process's runtime environment for retention telemetry (spec 044). The value is computed once at process startup from environment variables and filesystem probes, then cached for the lifetime of the process via DetectEnvKindOnce.
Serialization: always lowercase string. An invalid / empty value indicates a programming error; the payload builder will reject it.
const ( // EnvKindInteractive: real human on a desktop OS (macOS/Windows) or // Linux with DISPLAY/TTY present. EnvKindInteractive EnvKind = "interactive" // EnvKindCI: any known CI runner env var set (GitHub Actions, GitLab CI, // Jenkins, CircleCI, etc.). EnvKindCI EnvKind = "ci" // EnvKindCloudIDE: Codespaces, Gitpod, Replit, StackBlitz, Daytona, Coder. EnvKindCloudIDE EnvKind = "cloud_ide" // EnvKindContainer: /.dockerenv or /run/.containerenv present, or // $container env var set — with no CI/cloud-IDE markers. EnvKindContainer EnvKind = "container" // EnvKindHeadless: Linux with no DISPLAY/TTY and none of the above. EnvKindHeadless EnvKind = "headless" // EnvKindUnknown: classifier fell through all rules. EnvKindUnknown EnvKind = "unknown" )
func AllEnvKinds ¶ added in v0.25.0
func AllEnvKinds() []EnvKind
AllEnvKinds returns the canonical ordered list of EnvKind values. Used by tests for exhaustiveness checks and by the payload builder for validation.
type EnvMarkers ¶ added in v0.25.0
type EnvMarkers struct {
// HasCIEnv is true when any known CI env var is set (e.g., CI=true,
// GITHUB_ACTIONS, GITLAB_CI, JENKINS_URL, CIRCLECI).
HasCIEnv bool `json:"has_ci_env"`
// HasCloudIDEEnv is true when a cloud-IDE env var is set (e.g.,
// CODESPACES, GITPOD_WORKSPACE_ID, REPL_ID, STACKBLITZ_WORKER,
// DAYTONA_WORKSPACE_ID, CODER_AGENT_TOKEN).
HasCloudIDEEnv bool `json:"has_cloud_ide_env"`
// IsContainer is true when /.dockerenv or /run/.containerenv exists, or
// the $container env var is set.
IsContainer bool `json:"is_container"`
// HasTTY is true when stdin is attached to a terminal.
HasTTY bool `json:"has_tty"`
// HasDisplay is true when DISPLAY or WAYLAND_DISPLAY env var is set
// (Linux desktop session indicator).
HasDisplay bool `json:"has_display"`
}
EnvMarkers carries the raw boolean observations feeding the EnvKind decision tree (spec 044). All fields MUST remain Go bool values: the anonymity scanner re-asserts this on the serialized JSON to prevent a future refactor from accidentally widening any of these to a string or number.
Only booleans are allowed here. Do NOT add fields that carry env-var values, hostnames, usernames, or paths — those are PII by policy.
type ErrorCategory ¶ added in v0.24.0
type ErrorCategory string
ErrorCategory is a typed enum of error categories that telemetry will count. Only values defined here may be recorded; unknown categories are silently dropped by RecordError to prevent free-text error messages from leaking into telemetry.
const ( ErrCatOAuthRefreshFailed ErrorCategory = "oauth_refresh_failed" ErrCatOAuthTokenExpired ErrorCategory = "oauth_token_expired" ErrCatUpstreamConnectTimeout ErrorCategory = "upstream_connect_timeout" ErrCatUpstreamConnectRefused ErrorCategory = "upstream_connect_refused" ErrCatUpstreamHandshakeFailed ErrorCategory = "upstream_handshake_failed" ErrCatToolQuarantineBlocked ErrorCategory = "tool_quarantine_blocked" ErrCatDockerPullFailed ErrorCategory = "docker_pull_failed" ErrCatDockerRunFailed ErrorCategory = "docker_run_failed" ErrCatIndexRebuildFailed ErrorCategory = "index_rebuild_failed" ErrCatConfigReloadFailed ErrorCategory = "config_reload_failed" ErrCatSocketBindFailed ErrorCategory = "socket_bind_failed" )
type FeatureFlagSnapshot ¶ added in v0.24.0
type FeatureFlagSnapshot struct {
EnableSocket bool `json:"enable_socket"`
EnableWebUI bool `json:"enable_web_ui"`
EnablePrompts bool `json:"enable_prompts"`
RequireMCPAuth bool `json:"require_mcp_auth"`
EnableCodeExecution bool `json:"enable_code_execution"`
QuarantineEnabled bool `json:"quarantine_enabled"`
SensitiveDataDetectionEnabled bool `json:"sensitive_data_detection_enabled"`
OAuthProviderTypes []string `json:"oauth_provider_types"`
// Schema v3: DockerAvailable reports whether the host has a reachable
// Docker daemon, as observed by the runtime's checkDockerDaemon probe.
// Populated by the telemetry service at heartbeat time (not by
// BuildFeatureFlagSnapshot) so the snapshot helper stays side-effect-free
// and doesn't shell out to `docker info`.
DockerAvailable bool `json:"docker_available"`
}
FeatureFlagSnapshot captures the boolean / enum feature flags reported in the daily heartbeat. Spec 042 User Story 4.
func BuildFeatureFlagSnapshot ¶ added in v0.24.0
func BuildFeatureFlagSnapshot(cfg *config.Config) *FeatureFlagSnapshot
BuildFeatureFlagSnapshot returns a snapshot of the current feature flag state. It records boolean flags and a sorted, deduplicated list of OAuth provider TYPES (not URLs, client IDs, or tenant identifiers). The empty list is returned if no upstream servers have OAuth configured.
type FeedbackContext ¶
type FeedbackContext struct {
Version string `json:"version"`
Edition string `json:"edition"`
OS string `json:"os"`
Arch string `json:"arch"`
ServerCount int `json:"server_count"`
ConnectedServerCount int `json:"connected_server_count"`
RoutingMode string `json:"routing_mode"`
}
FeedbackContext provides automatic system context alongside feedback.
type FeedbackRequest ¶
type FeedbackRequest struct {
Category string `json:"category"` // bug, feature, other
Message string `json:"message"`
Email string `json:"email,omitempty"`
Context FeedbackContext `json:"context"`
}
FeedbackRequest is the user-submitted feedback payload.
type FeedbackResponse ¶
type FeedbackResponse struct {
Success bool `json:"success"`
IssueURL string `json:"issue_url,omitempty"`
Error string `json:"error,omitempty"`
}
FeedbackResponse is the response from the telemetry backend.
type FileProber ¶ added in v0.25.0
type FileProber interface {
// Exists returns true if the file at path exists (any type: file, dir,
// symlink). Errors other than "not exists" are treated as "exists" to
// avoid mis-classifying a container due to permission issues.
Exists(path string) bool
}
FileProber abstracts filesystem existence checks so the env_kind detector can be unit-tested with a fake FS. Production uses defaultFileProber.
type HandshakeChecker ¶ added in v0.25.0
type HandshakeChecker interface {
LaunchedViaTray() bool
}
HandshakeChecker abstracts the tray-socket handshake that signals launched_via=tray. Tests inject a constant answer; production wiring is a no-op (defaultHandshakeChecker) until tray→core handshake is added.
type HeartbeatPayload ¶
type HeartbeatPayload struct {
// v1 fields (preserved unchanged)
AnonymousID string `json:"anonymous_id"`
Version string `json:"version"`
Edition string `json:"edition"`
OS string `json:"os"`
Arch string `json:"arch"`
GoVersion string `json:"go_version"`
ServerCount int `json:"server_count"`
ConnectedServerCount int `json:"connected_server_count"`
ToolCount int `json:"tool_count"`
UptimeHours int `json:"uptime_hours"`
RoutingMode string `json:"routing_mode"`
QuarantineEnabled bool `json:"quarantine_enabled"`
Timestamp string `json:"timestamp"`
// Spec 042 (Tier 2) additions
SchemaVersion int `json:"schema_version,omitempty"`
AnonymousIDCreatedAt string `json:"anonymous_id_created_at,omitempty"`
CurrentVersion string `json:"current_version,omitempty"`
PreviousVersion string `json:"previous_version"`
LastStartupOutcome string `json:"last_startup_outcome,omitempty"`
SurfaceRequests map[string]int64 `json:"surface_requests,omitempty"`
BuiltinToolCalls map[string]int64 `json:"builtin_tool_calls,omitempty"`
UpstreamToolCallCountBucket string `json:"upstream_tool_call_count_bucket,omitempty"`
RESTEndpointCalls map[string]map[string]int64 `json:"rest_endpoint_calls,omitempty"`
FeatureFlags *FeatureFlagSnapshot `json:"feature_flags,omitempty"`
ErrorCategoryCounts map[string]int64 `json:"error_category_counts,omitempty"`
DoctorChecks map[string]DoctorCounts `json:"doctor_checks,omitempty"`
// Schema v3 additions.
// ServerProtocolCounts is a fixed-enum histogram over cfg.Servers by
// Protocol. Keys are exactly: stdio, http, sse, streamable_http, auto.
// Never contains server names, URLs, or unknown values (unknown/empty
// protocols bucket into "auto").
ServerProtocolCounts map[string]int `json:"server_protocol_counts,omitempty"`
// ServerDockerIsolatedCount is the number of configured servers the
// runtime actually wraps in Docker isolation. Distinct from "has Docker
// available" — an install can have Docker but never use it for isolation.
ServerDockerIsolatedCount int `json:"server_docker_isolated_count,omitempty"`
// Spec 044 additions. schema_version stays at 3 (set by spec 042); these
// fields are additive and forward-compatible.
//
// EnvKind: ground-truth classification of the process environment, computed
// once at startup by DetectEnvKindOnce. One of the EnvKind* constants.
EnvKind string `json:"env_kind,omitempty"`
// EnvMarkers: raw boolean observations feeding EnvKind. Fields are ALL
// booleans — the anonymity scanner re-asserts this on the serialized form.
EnvMarkers *EnvMarkers `json:"env_markers,omitempty"`
// Activation is the retention funnel snapshot: monotonic first-ever flags,
// 24h sliding counters, and bucketed token-savings estimate. Loaded from
// the BBolt activation bucket at heartbeat build time. nil when the store
// is not wired (e.g. in short-lived CLI commands).
Activation *ActivationState `json:"activation,omitempty"`
// LaunchSource: how the process was launched. One of "installer", "tray",
// "login_item", "cli", "unknown". Detected once at startup via
// DetectLaunchSourceOnce, with a one-shot "installer" override driven by
// the installer_heartbeat_pending BBolt flag (cleared after first heartbeat).
LaunchSource string `json:"launch_source,omitempty"`
// AutostartEnabled: tri-state login-item status.
// - *true : tray reports the app IS registered as a login item
// - *false : tray reports the app is NOT registered
// - nil : unknown (tray not running, Linux, or sidecar absent)
// Pointer intentional so JSON null is distinguishable from false —
// receivers need this to separate "user disabled" from "we don't know".
AutostartEnabled *bool `json:"autostart_enabled"`
}
HeartbeatPayload is the anonymous telemetry payload sent periodically. Spec 042 expanded the payload with Tier 2 fields; v1 fields are preserved.
type LaunchSource ¶ added in v0.25.0
type LaunchSource string
LaunchSource identifies how the current mcpproxy process was launched, for retention telemetry (spec 044). Detection happens once at process startup (via DetectLaunchSourceOnce, added in a later task), with a one-shot "installer" override driven by the installer_heartbeat_pending BBolt flag.
Serialization: always lowercase string. The payload builder rejects values outside the canonical set.
const ( // LaunchSourceInstaller: MCPPROXY_LAUNCHED_BY=installer env var at // startup. One-shot: cleared after first heartbeat. LaunchSourceInstaller LaunchSource = "installer" // LaunchSourceTray: the tray socket handshake signalled launched_via=tray. LaunchSourceTray LaunchSource = "tray" // LaunchSourceLoginItem: OS launched the process as a registered login // item (parent = launchd on macOS, explorer.exe on Windows via the Run // registry key). LaunchSourceLoginItem LaunchSource = "login_item" // LaunchSourceCLI: stdin is a TTY and no other rule matched. LaunchSourceCLI LaunchSource = "cli" // LaunchSourceUnknown: none of the above. LaunchSourceUnknown LaunchSource = "unknown" )
func AllLaunchSources ¶ added in v0.25.0
func AllLaunchSources() []LaunchSource
AllLaunchSources returns the canonical ordered list of LaunchSource values.
func DetectLaunchSource ¶ added in v0.25.0
func DetectLaunchSource(env map[string]string, handshake HandshakeChecker, ppid PPIDChecker, tty TTYChecker) LaunchSource
DetectLaunchSource is the pure classifier implementing the precedence rules from research.md R3:
- MCPPROXY_LAUNCHED_BY=installer env var → installer
- handshake.LaunchedViaTray() → tray
- ppid.IsLoginItemParent() → login_item
- tty.IsTerminal() → cli
- fallthrough → unknown
All parameters are nil-safe: a nil HandshakeChecker / PPIDChecker / TTYChecker simply contributes "false" to its respective branch.
func DetectLaunchSourceOnce ¶ added in v0.25.0
func DetectLaunchSourceOnce() LaunchSource
DetectLaunchSourceOnce classifies the current process's launch source exactly once per process lifetime and caches the result.
type PPIDChecker ¶ added in v0.25.0
type PPIDChecker interface {
IsLoginItemParent() bool
}
PPIDChecker abstracts the OS-specific "is my parent a login-item launcher?" check. On macOS the production impl verifies parent process name == "launchd"; on Windows parent == "explorer.exe"; otherwise false.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter enforces a maximum number of requests per hour.
func NewRateLimiter ¶
func NewRateLimiter(maxPerHour int) *RateLimiter
NewRateLimiter creates a rate limiter with the given max requests per hour.
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow() bool
Allow returns true if the request is within the rate limit.
type RegistrySnapshot ¶ added in v0.24.0
type RegistrySnapshot struct {
SurfaceCounts map[string]int64 `json:"surface_requests"`
BuiltinToolCalls map[string]int64 `json:"builtin_tool_calls"`
UpstreamToolCallCountBucket string `json:"upstream_tool_call_count_bucket"`
RESTEndpointCalls map[string]map[string]int64 `json:"rest_endpoint_calls"`
ErrorCategoryCounts map[string]int64 `json:"error_category_counts"`
DoctorChecks map[string]DoctorCounts `json:"doctor_checks"`
}
RegistrySnapshot is an immutable view of the registry built by Snapshot(). It is safe to mutate the maps in a snapshot — they are copies.
type RuntimeStats ¶
type RuntimeStats interface {
GetServerCount() int
GetConnectedServerCount() int
GetToolCount() int
GetRoutingMode() string
IsQuarantineEnabled() bool
// Schema v3 additions.
// IsDockerAvailable reports whether the host has a reachable Docker
// daemon. Implementations should memoize the probe result (running
// `docker info` on every heartbeat has cost) and return the cached value.
IsDockerAvailable() bool
// GetDockerIsolatedServerCount returns how many currently-configured
// servers the runtime is actually wrapping in a Docker container.
GetDockerIsolatedServerCount() int
}
RuntimeStats is an interface to decouple from the runtime package.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service manages anonymous telemetry heartbeats and feedback submission.
func (*Service) ActivationDB ¶ added in v0.25.0
ActivationDB returns the BBolt DB handle associated with the activation store (or nil). Callers pair this with ActivationStore() to perform writes.
func (*Service) ActivationStore ¶ added in v0.25.0
func (s *Service) ActivationStore() ActivationStore
ActivationStore returns the wired store (or nil). Used by MCP and runtime integration points that need to increment counters.
func (*Service) BuildPayload ¶ added in v0.24.0
func (s *Service) BuildPayload() HeartbeatPayload
BuildPayload renders the heartbeat payload at the current point in time. It is exported so the `mcpproxy telemetry show-payload` command can render the same payload that would next be sent, without making a network call.
func (*Service) EnvDisabledReason ¶ added in v0.24.0
func (s *Service) EnvDisabledReason() EnvDisabledReason
EnvDisabledReason returns the env-var reason telemetry is disabled, if any.
func (*Service) Registry ¶ added in v0.24.0
func (s *Service) Registry() *CounterRegistry
Registry returns the counter registry for Tier 2 telemetry events. Always non-nil after New, even if telemetry is disabled — that way callers can always Record* without nil checks; the data simply never leaves the process.
func (*Service) SetActivationStore ¶ added in v0.25.0
func (s *Service) SetActivationStore(store ActivationStore, db *bbolt.DB)
SetActivationStore wires the BBolt-backed activation store and the shared DB handle (Spec 044). Optional; when unset, heartbeat payloads omit the activation object entirely. Safe to call once during startup.
func (*Service) SetAutostartReader ¶ added in v0.25.0
func (s *Service) SetAutostartReader(r *AutostartReader)
SetAutostartReader overrides the default autostart reader (for tests). In production the first heartbeat lazy-initializes DefaultAutostartReader.
func (*Service) SetConfiguredIDECountProvider ¶ added in v0.25.0
SetConfiguredIDECountProvider wires a function that returns the number of IDE client config files mcpproxy has registered itself into (Spec 044). Typically supplied by internal/connect.Service.
func (*Service) SetRuntimeStats ¶
func (s *Service) SetRuntimeStats(stats RuntimeStats)
SetRuntimeStats sets the runtime stats provider (called after runtime is fully initialized).
func (*Service) Start ¶
Start begins the telemetry heartbeat loop. This is a blocking call; run in a goroutine.
func (*Service) SubmitFeedback ¶
func (s *Service) SubmitFeedback(ctx context.Context, req *FeedbackRequest) (*FeedbackResponse, error)
SubmitFeedback sends feedback to the telemetry backend.
type Surface ¶ added in v0.24.0
type Surface int
Surface identifies which client surface originated a request.
func ParseClientSurface ¶ added in v0.24.0
ParseClientSurface maps the X-MCPProxy-Client header value to a Surface enum. The expected format is "<surface>/<version>"; unknown prefixes and missing headers map to SurfaceUnknown.
type TTYChecker ¶ added in v0.25.0
type TTYChecker interface {
IsTerminal() bool
}
TTYChecker abstracts stdin-is-a-terminal detection so the env_kind detector can be unit-tested without attaching a real TTY.