Privacy and redaction

MCP tool calls can carry sensitive payloads — API tokens, customer PII, raw model output. The SDK applies a layered pipeline before any event leaves your process. This page explains what's stripped automatically, what you can hook into, and what's never sent in the first place.

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.

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

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>].
  • Long base64-looking strings (≥10KB) → replaced with "[binary data redacted...]".
  • Keys matching the sensitive-key patternauthorization, 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]".

This stage is not configurable. It's safety net behavior — if you have stricter requirements, encode them in beforeSend.

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. beforeSend (optional)

beforeSend runs on each fully-built PostHog payload — { event, distinct_id, properties } — right before it's sent, once per emitted event (including the $exception sibling). It mirrors the beforeSend hook in posthog-node and may be sync or async.

  • Return the (possibly mutated) event to send it.
  • Return a nullish value (null/undefined) to drop that event.
  • A thrown error also drops that event.
TypeScript
instrument(server, posthog, {
beforeSend: (event) => {
if (event.event === "$exception") return null // drop
return event
},
})

Because it runs after sanitization and truncation, anything you mutate here is the final word before the wire.

Exception autocapture

By default the SDK emits an $exception sibling event whenever a tool call fails (throws or returns isError: true). Set enableExceptionAutocapture: false to suppress that sibling — the $mcp_tool_call still records $mcp_is_error, but no $exception event is sent.

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

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 mint a person profile. When identify() resolves an identity for a session, person processing stays on and events attribute to that user. See Identifying users.

Disabling capture entirely

To turn capture off without removing the wrapper, return a nullish value from beforeSend to drop every event. If you only want session/timing metadata and no payload, drop or strip the payload fields in beforeSend instead:

TypeScript
beforeSend: (event) => {
delete event.properties.$mcp_parameters
delete event.properties.$mcp_response
return event
},
Drain the queue before exit

You own the posthog-node client's lifecycle. Call posthog.shutdown() (or posthog.flush()) from your SIGTERM / beforeExit handler — or explicitly at the end of each serverless invocation — so in-flight events aren't dropped. See Installation for the snippet.

Buffering and back-pressure

The in-memory queue is owned by the posthog-node client you pass in. If it overflows or fails to flush, events are dropped with a warning surfaced to your logger.

Logging

MCP servers commonly speak over stdio, where any write to console.* corrupts the protocol stream. The SDK defaults its logger to a no-op for that reason — wire your own in development so warnings (swallowed identify errors, dropped batches) become visible:

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

Errors thrown from your beforeSend, identify, intentFallback, and eventProperties callbacks are swallowed and routed to the logger — they never surface to the agent or interrupt tool execution. (A beforeSend that throws additionally drops the event it was inspecting.)

Community questions

Was this page useful?

Questions about this page? or post a community question.