diff --git a/contents/docs/mcp-analytics/events.mdx b/contents/docs/mcp-analytics/events.mdx index 6d374b680ff2..3238e4c029a7 100644 --- a/contents/docs/mcp-analytics/events.mdx +++ b/contents/docs/mcp-analytics/events.mdx @@ -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 @@ -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` | @@ -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 diff --git a/contents/docs/mcp-analytics/privacy.mdx b/contents/docs/mcp-analytics/privacy.mdx index 48059c036e7e..229686f5dee2 100644 --- a/contents/docs/mcp-analytics/privacy.mdx +++ b/contents/docs/mcp-analytics/privacy.mdx @@ -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. @@ -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`. diff --git a/contents/docs/mcp-analytics/sdk-v2.mdx b/contents/docs/mcp-analytics/sdk-v2.mdx index 3de63cffebeb..fcaf3244ce34 100644 --- a/contents/docs/mcp-analytics/sdk-v2.mdx +++ b/contents/docs/mcp-analytics/sdk-v2.mdx @@ -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 @@ -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. | diff --git a/contents/docs/mcp-analytics/start-here.mdx b/contents/docs/mcp-analytics/start-here.mdx index 0d374f318ea1..c7b013e6b18d 100644 --- a/contents/docs/mcp-analytics/start-here.mdx +++ b/contents/docs/mcp-analytics/start-here.mdx @@ -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, {