Privacy and redaction

Contents

MCP tool calls can contain API tokens, personal data, and model output. The SDK sanitizes and truncates events before sending them. Use this page to understand automatic redaction and configure additional filtering.

What never leaves your process

The SDK does not capture:

  • Your PostHog API key or any environment variables.
  • The transport itself (TCP/WebSocket frames, MCP framing internals).
  • Tool source code, function references, or closures.
  • The full content of image or audio content blocks (replaced with a text stub).
  • The content of resource blocks with a blob payload.
  • Resource bodies. The SDK never captures resources/read results. It captures only the URI, timing, and error state. Resource listings contain discovery metadata, such as names, URIs, and MIME types, so the SDK captures them.

$mcp_tool_call payloads include $mcp_parameters (the request arguments) and $mcp_response (the tool's result), after the pipeline below.

When the SDK can confirm that it owns an injected analytics argument, it removes the argument before the tool handler runs. On supported high-level server paths, context, conversation_id, and llm_model don't appear in $mcp_parameters. Their accepted values are captured separately as $mcp_intent, $mcp_conversation_id, and $mcp_llm_model.

$mcp_llm_model comes from recognized client metadata or the agent's self-report. $mcp_llm_model_source distinguishes the two. Neither source is verified by the MCP protocol. Don't use either for billing, authorization, or other security decisions.

Model capture and conversation IDs are enabled by default. Conversation IDs add a visible JSON text block to eligible tool results. See how to disable them.

The redaction pipeline

Every event runs through these stages before being sent to PostHog:

1. Automatic sanitization

The SDK runs a deterministic sanitizer:

  • Image/audio content blocks -> replaced with [image redacted: <mime>] or [audio redacted: <mime>].
  • Resource blocks with a .blob -> replaced with [resource redacted: <mime>].
  • Go stubs have no MIME type. They read [image content redacted - not supported by PostHog MCP analytics], [audio content redacted - not supported by PostHog MCP analytics], and [binary resource content redacted - not supported by PostHog MCP analytics].
  • Long base64-looking strings (>=10KB) -> replaced with "[binary data redacted...]".
  • Keys matching the sensitive-key pattern – authorization, cookie, password, token, secret, api_key, private_key, and similar – have their values replaced with "[redacted]".
  • PostHog API key patterns (ph[a-z]_…) in any string value -> replaced with "[redacted]".
  • Credentials inside URLs – replaced with [redacted]. This applies to string values in resource names, parameters, responses, and exception messages. See the URL rules below.
  • Credential-looking words – detected through entropy and known formats, such as sk-… and PEM markers. The SDK replaces these words in parameters, responses, intent, and exception messages. It preserves surrounding diagnostic text.

Automatic sanitization is not configurable. It detects known key formats and sensitive property names, but it cannot detect every secret. Other text, including exception messages, remains unchanged. Use your before-send hook for additional filtering.

URL credentials

The sanitizer replaces user:password@ and values of query or fragment fields that identify credentials. It checks field-name segments separated by -, _, ., /, or ;.

Recognized names include auth, token, secret, password, key, signature, sig, jwt, and session. Signed URL names include X-Amz-Signature, AWSAccessKeyId, GoogleAccessId, Policy, and code. This intentionally also redacts some benign fields, such as sort_key.

The sanitizer also handles these URL forms:

  • URLs nested one level inside a retained value
  • URIs without an authority, such as resource:guide?token=…
  • Fragments used for routing
  • Adjacent addresses with no separating whitespace

When a credential boundary is ambiguous, the sanitizer removes more text. It preserves URLs that need no redaction byte-for-byte. It replaces URLs longer than 8 KB or with more than 128 fields entirely.

2. Truncation

After sanitization, the payload is truncated to fit within PostHog ingestion limits:

  • Per-field caps applied to large strings.
  • Recursive normalization: max depth 10, max breadth 100, max string 32 KB.
  • A 100 KB total event budget, with progressive falloff if the budget is exceeded.

If a payload would exceed the budget, the SDK truncates rather than drops. The truncation markers are visible in the captured $mcp_parameters / $mcp_response.

3. Your before-send hook (optional)

The hook runs on each fully built payload right before it's sent, once per event, including the $exception event of a failed call. It runs after sanitization and truncation, so its changes are final.

  • Return the event, changed or not, to send it.
  • Return nothing (null, None, or nil) to drop it.
  • An error in the hook also drops the event.
instrument(server, posthog, {
beforeSend: (event) => {
if (event.event === "$exception") return null // drop
delete event.properties.$mcp_parameters
delete event.properties.$mcp_response
return event
},
})

Removing $mcp_parameters and $mcp_response keeps tool arguments and results out of PostHog. Intent, error messages, exception details, and custom properties stay unless you remove them too.

Language notes:

  • Python: MCPAnalyticsOptions.before_send can be sync or async. On a PostHogMCP dispatcher, pass before_send= to the client instead. That callback must be synchronous.
  • Go: the hook is the posthog-go client's BeforeSend, so it also sees your app's other events. It receives the $exception event as a posthog.Exception and every other event as a posthog.Capture.
  • TypeScript: on a PostHogMCP dispatcher, pass beforeSend to the client constructor.

Dropping a tool call event doesn't drop its $exception event. Check for $exception too if you want both gone.

Turn off exception events

By default, each failed tool call also sends a $exception event to Error Tracking. Turn it off and the $mcp_tool_call event still records $mcp_is_error:

instrument(server, posthog, { enableExceptionAutocapture: false })

On custom dispatchers, use mcp_exception_autocapture=False on Python's PostHogMCP and mcp_exception_autocapture: false on Ruby's PostHog::MCP::Client.

Anonymous sessions and person profiles

Events for sessions with no resolved identity are sent with $process_person_profile: false, so anonymous MCP sessions don't each create a person profile. When your identify callback resolves an identity, events attribute to that user. See Identifying users.

Buffering

Events queue in the PostHog client you pass in. If the queue overflows or fails to flush, events are dropped. Send queued events before the process stops, as each installation page shows.

Logging

MCP servers often use stdio, where writing to stdout corrupts the protocol stream. The SDKs' default loggers write nothing. Pass a logger during development to see callback errors and warnings, such as logger in TypeScript, Python, and Ruby, or WithErrorHandler in Go. The Go adapter's WithErrorHandler default is a no-op, but the posthog-go client logs to stderr by default. Set Config.Logger to change that:

TypeScript
instrument(server, posthog, {
logger: (message) => fs.appendFileSync("/tmp/mcp.log", message + "\n"),
})

The SDK catches errors from your callbacks, such as identify, intent fallback, event properties, and before-send. It reports them without interrupting the tool call. If the before-send hook fails, the SDK also drops that event.

Still have questions?

Was this page useful?