# Privacy and redaction - Docs

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 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]"`.

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`](/docs/libraries/node.md) 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

PostHog AI

```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

PostHog AI

```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](/docs/mcp-analytics/identifying-users.md).

## 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

PostHog AI

```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](/docs/mcp-analytics/installation.md#graceful-shutdown) 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

PostHog AI

```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

Ask a question

### Was this page useful?

HelpfulCould be better