Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions contents/docs/mcp-analytics/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ This page is the wire-level contract for the MCP Analytics SDKs. TypeScript-only
| ------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `$mcp_tool_call` | Every `tools/call` request | `$mcp_tool_name`, `$mcp_tool_description`, `$mcp_parameters`, `$mcp_response`, `$mcp_duration_ms`, `$mcp_is_error`, `$mcp_error_type`/`$mcp_error_message` (on errors), optionally `$mcp_intent`/`$mcp_intent_source` and `$mcp_llm_model`/`$mcp_llm_model_source` |
| `$mcp_tools_list` | Every `tools/list` response | `$mcp_listed_tool_names` (string[] of advertised tool names), `$mcp_response` (the response envelope as sent, including `nextCursor` and the 2026-07-28 `ttlMs`/`cacheScope` directives) |
| `$mcp_resources_list` | Every `resources/list` and `resources/templates/list` request | `$mcp_response` (the listing as sent: names, URIs, URI templates, MIME types, `nextCursor`), `$mcp_duration_ms`, `$mcp_is_error`. `$mcp_parameters.request.method` tells the two listings apart. |
| `$mcp_resource_read` | Every `resources/read` request | `$mcp_resource_name` (the URI, with credentials redacted), `$mcp_parameters`, `$mcp_duration_ms`, `$mcp_is_error`, `$mcp_error_type`/`$mcp_error_message` (on errors). The resource body is never captured. |
| `$mcp_initialize` | Every `2025-11-25` client/server handshake | `$mcp_client_name`, `$mcp_client_version`, `$mcp_server_name`, `$mcp_server_version`, `$mcp_protocol_version` |
| _(your event name)_ | A call to `analytics.capture({ event, properties })` | Sent under the verbatim `event` name you pass (a customer event, not `$`-prefixed), with your `properties` merged in. See [Custom events](/docs/mcp-analytics/custom-events). |
| `$mcp_missing_capability` | The `get_more_tools` virtual tool is invoked (`reportMissing: true`) | The agent's reasoning is captured as `$mcp_intent`. See [Tracking missing capabilities](/docs/mcp-analytics/missing-capability). |
| `$identify` | `identify()` returns a new identity for a session | `$set` populated from the identity's `properties` |
| `$exception` | Sibling event whenever a tool errors (unless `enableExceptionAutocapture: false`) | `$exception_list`, `$exception_level`, plus the same `$mcp_*` context as the main event |
| `$exception` | Sibling event whenever a tool call or resource request errors (unless `enableExceptionAutocapture: false`) | `$exception_list`, `$exception_level`, plus the same `$mcp_*` context as the main event |

The event enum reserves `$mcp_resources_list`, `$mcp_resource_read`, `$mcp_prompts_list`, and `$mcp_prompt_get`, but the server wrappers don't emit them yet. Resource and prompt payloads still pass through unchanged.
Resource events require `@posthog/mcp` ≥ 0.15.0 or `posthog` ≥ 7.51.0. The event enum also reserves `$mcp_prompts_list` and `$mcp_prompt_get`, but the server wrappers don't emit them yet; prompt payloads pass through unchanged.

## Core properties

Expand All @@ -28,13 +30,13 @@ Present on most `mcp_*` events.
| ------------------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `$session_id` | string | The MCP session id (`ses_<32-hex>`), resolved per request, first match winning: (1) the agent's `conversation_id` argument, when [`enableConversationId`](/docs/mcp-analytics/conversation-id) is on — the only id that survives reconnects, restarts, and per-request server instances; (2) a session id the request itself carried, on the 2025-11-25 revision; (3) the id the server instance already holds, rotated after 30 minutes of inactivity. Steps 1 and 3 are what apply on the 2026-07-28 revision, which removed protocol-level sessions. Derivation is deterministic and unsalted, so two pods that share no state agree on the same session. |
| `$mcp_source` | string | Always `"posthog_mcp_analytics"`. Use this to filter out non-MCP events when querying mixed projects. |
| `$mcp_resource_name` | string | Tool, resource, or prompt name |
| `$mcp_resource_name` | string | Tool or prompt name, or on resource events the resource URI with its credentials redacted (see [Privacy](/docs/mcp-analytics/privacy)) |
| `$mcp_tool_name` | string | Same as `$mcp_resource_name`, but only on `$mcp_tool_call` |
| `$mcp_tool_description` | string | The tool's `description` at the moment of the call. Cached from `tools/list` and (for `McpServer`) seeded from `_registeredTools`. Only on `$mcp_tool_call` and the paired `$exception` event. |
| `$mcp_tool_category` | string | Your own grouping label for the tool, when you set one. Only on `$mcp_tool_call` and the paired `$exception` event. |
| `$mcp_listed_tool_names` | string[] | Names of tools advertised in a `tools/list` response. Only on `$mcp_tools_list`. Useful for joining against `$mcp_tool_call` via `$session_id` to find tools advertised but never called. |
| `$mcp_duration_ms` | number (ms) | Wall-clock duration of the tool call |
| `$mcp_is_error` | boolean | True if the tool threw or returned `isError: true` |
| `$mcp_duration_ms` | number (ms) | Wall-clock duration of the tool call or resource request |
| `$mcp_is_error` | boolean | True if the handler threw or returned `isError: true` |
| `$mcp_error_type` | string | Low-cardinality failure category, so you can break errors down by cause without joining to the `$exception` sibling. Defaults to the thrown error's type; pass an explicit label to categorize failures yourself (e.g. `validation`, `permission`, `timeout`, `rate_limited`). Only set when `$mcp_is_error` is true. |
| `$mcp_error_message` | string | The failed call's error message, truncated and passed through the same redaction as `$mcp_parameters` and `$mcp_response`. Only set when `$mcp_is_error` is true. |
| `$mcp_server_name` | string | `server._serverInfo.name` |
Expand All @@ -49,7 +51,7 @@ Present on most `mcp_*` events.
| `$mcp_llm_model` | string | The model identifier the agent supplied through the SDK-injected `llm_model` argument. TypeScript only and present when `captureModel` is enabled and the agent doesn't answer `unknown`. The value is unverified. Use it for analytics, not billing or security. |
| `$mcp_llm_model_source` | `"self_reported"` | How the model identifier was obtained. Always `"self_reported"` today. TypeScript only. |
| `$mcp_parameters` | object | Sanitized request arguments. When the TypeScript wrapper can confirm that it owns an injected argument, this excludes `context`, `conversation_id`, and `llm_model`. |
| `$mcp_response` | object | Sanitized tool result |
| `$mcp_response` | object | Sanitized tool result, or the listing on `$mcp_tools_list` and `$mcp_resources_list`. Never a resource body. |
| `$mcp_conversation_id` | string | Present when `enableConversationId` is on. See [Conversation IDs](/docs/mcp-analytics/conversation-id). |

### How the harness label is resolved
Expand Down
2 changes: 2 additions & 0 deletions contents/docs/mcp-analytics/privacy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The SDK does not capture:
- 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. A `resources/read` result is never captured; `$mcp_resource_read` carries only the URI, timing, and error state. Listings (`resources/list`, `resources/templates/list`) are captured, since names, URIs, and MIME types are discovery metadata rather than content.

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

Expand All @@ -35,6 +36,7 @@ The SDK runs a deterministic sanitizer:
- **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**, in any string value (`$mcp_resource_name`, `$mcp_parameters`, `$mcp_response`, exception messages) → the `user:password@` part and the values of credential-named query and fragment fields are replaced with `[redacted]`. A field name counts as credential-named when any `-`/`_`/`.`/`/`/`;`-separated segment of it is `auth`, `token`, `secret`, `password`, `key`, `signature`, `sig`, `jwt`, `session`, and similar, plus the exact names signed URLs use (`X-Amz-Signature`, `AWSAccessKeyId`, `GoogleAccessId`, `Policy`, `code`). This over-redacts a benign `sort_key` by design. URLs nested one level inside a retained value, authority-less URIs such as `resource:guide?token=…`, hash-routed fragments, and adjacent addresses run together without whitespace are all covered, and wherever a credential's boundary is ambiguous the sanitizer redacts more rather than less. A URL with nothing to redact is returned byte-for-byte; URLs over 8 KB or 128 fields are replaced whole.
- **Credential-looking words** — detected by entropy and known key formats (`sk-…`, PEM markers) — replaced word by word, in `$mcp_parameters`, `$mcp_response`, the captured intent, and exception messages. A message like `auth failed for sk-…` keeps its diagnostic text and loses only the key.

This stage is not configurable, and it is a safety net rather than a general-purpose credential scrubber: it catches known key formats and obviously sensitive keys, not every secret your tools might echo. Free text your server writes — exception messages especially — is passed through as-is once those patterns are gone. If you have stricter requirements, encode them in `beforeSend`.
Expand Down
3 changes: 1 addition & 2 deletions contents/docs/mcp-analytics/sdk-v2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ This works on `2025-11-25` and `2026-07-28`. The value is unverified, so use it

`@posthog/mcp` preserves MCP App tool metadata, `ui://` resources, structured tool output, result metadata, HTML, and content security policy metadata on both supported revisions. On the tested high-level `McpServer` path, injected `context` and `llm_model` arguments don't reach the App handler.

The App's tool call is captured with its intent, self-reported model, and protocol version. Automatic analytics for the App's `resources/list` and `resources/read` requests on the tested high-level server path haven't shipped yet. The resource payloads still pass through unchanged.
The App's tool call is captured with its intent, self-reported model, and protocol version. The App's `resources/list`, `resources/templates/list`, and `resources/read` requests emit `$mcp_resources_list` and `$mcp_resource_read` on both server paths; the resource payloads pass through unchanged and a read's body is never captured.

## Not instrumented yet

Expand All @@ -123,7 +123,6 @@ These gaps apply to the TypeScript and Python SDKs alike.
| **Tasks** (`io.modelcontextprotocol/tasks`) | A tool returning a task handle records an instant success, so task-based tools look fast and always-succeeding. |
| **Multi round-trip** (`resultType: "input_required"`) | Each round counts as its own `$mcp_tool_call`, inflating call counts and durations. |
| `server/discover` | Not captured — no session-start event on this revision. |
| MCP App resource requests on the high-level server path | App resources pass through unchanged, but `resources/list` and `resources/read` don't emit analytics yet. |
| `Mcp-Method` / `Mcp-Name` headers | Not read. |
| `clientCapabilities` in `_meta` | Not captured. `clientInfo` and protocol version are. |

Expand Down
2 changes: 1 addition & 1 deletion contents/docs/mcp-analytics/start-here.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ subtitle="Required"
icon="IconLock"
>

The SDK runs every event through automatic sanitization (image/audio/binary stubs, sensitive-key masking like `authorization`, `cookie`, `password`, PostHog key patterns) and truncation to fit ingestion limits. For full control, add a `beforeSend` hook that runs on each built PostHog payload right before it's sent – mutate and return the event to send it, or return a nullish value to drop it.
The SDK runs every event through automatic sanitization (image/audio/binary stubs, sensitive-key masking like `authorization`, `cookie`, `password`, PostHog key patterns, credentials inside URLs) and truncation to fit ingestion limits. For full control, add a `beforeSend` hook that runs on each built PostHog payload right before it's sent – mutate and return the event to send it, or return a nullish value to drop it.

```ts
instrument(server, posthog, {
Expand Down
Loading