From 9ceaebe63561734685413f999df23907071c7c41 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 7 Oct 2026 11:04:06 -0300 Subject: [PATCH 1/8] docs(mcp-analytics): add an install path for languages without an SDK Adds /docs/mcp-analytics/installation/other-languages: the $mcp_tool_call contract a server in any language can send through its PostHog SDK or the capture API, with the context argument for intent, session ID rules, failure fields, a redaction checklist for payloads, and a SQL check. Derived from the sdk-specs mcp-analytics spec and the properties the mcp_analytics backend filters on ($mcp_source, $mcp_server_name). Links it from the sidebar, the installation index (as the coding-agent fallback when the wizard doesn't apply), and the event reference intro. Tested: ran the page's verification query on project 2 (126,015 calls in the last hour, all missing_/bad_ counts 0) and confirmed JSONType returns String for a "false" string and Bool for false. prettier --check passes. --- contents/docs/mcp-analytics/events.mdx | 2 +- .../docs/mcp-analytics/installation/index.mdx | 2 + .../installation/other-languages.mdx | 220 ++++++++++++++++++ src/navs/index.js | 4 + 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 contents/docs/mcp-analytics/installation/other-languages.mdx diff --git a/contents/docs/mcp-analytics/events.mdx b/contents/docs/mcp-analytics/events.mdx index a600d786fe3d..5c83d76b9548 100644 --- a/contents/docs/mcp-analytics/events.mdx +++ b/contents/docs/mcp-analytics/events.mdx @@ -4,7 +4,7 @@ title: Event and property reference import CalloutBox from "components/Docs/CalloutBox"; -This page is the wire-level contract for the MCP Analytics SDKs. TypeScript-only properties are marked below. All property keys are prefixed with `$mcp_*` so they never collide with PostHog autocapture, Web analytics, or other product events. +This page is the wire-level contract for the MCP Analytics SDKs, and for servers that [send the events themselves](/docs/mcp-analytics/installation/other-languages). TypeScript-only properties are marked below. All property keys are prefixed with `$mcp_*` so they never collide with PostHog autocapture, Web analytics, or other product events. ## Events diff --git a/contents/docs/mcp-analytics/installation/index.mdx b/contents/docs/mcp-analytics/installation/index.mdx index 34147529f4a9..5f1de5957d9c 100644 --- a/contents/docs/mcp-analytics/installation/index.mdx +++ b/contents/docs/mcp-analytics/installation/index.mdx @@ -12,6 +12,8 @@ The wizard detects your server, installs the SDK, and adds the instrumentation. +For a Go or Ruby server, follow that language's page below. For a language without an SDK, ask your coding agent to follow [`/docs/mcp-analytics/installation/other-languages.md`](/docs/mcp-analytics/installation/other-languages.md). + ## Install by hand Choose your server's language: diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx new file mode 100644 index 000000000000..f08c08761335 --- /dev/null +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -0,0 +1,220 @@ +--- +title: Other languages MCP Analytics installation +platformLabel: Other languages +platformIconName: IconCode +showStepsToc: true +--- + +import { Steps, Step } from 'components/Docs/Steps' + +MCP Analytics has SDKs for [TypeScript](/docs/mcp-analytics/installation/typescript), [Python](/docs/mcp-analytics/installation/python), [Go](/docs/mcp-analytics/installation/go), and [Ruby](/docs/mcp-analytics/installation/ruby). If your MCP server uses another language, such as Java, Kotlin, C#, Rust, PHP, Elixir, or Swift, you can still use MCP Analytics. The SDKs send ordinary PostHog events. Send events with the same shape and PostHog shows your server in MCP Analytics. + +This page describes the event your server sends for each tool call. + + + +You can follow this page to instrument an MCP server. First check the server's language. For TypeScript, Python, Go, or Ruby, use that language's installation page instead, because the SDK handles redaction and sessions for you. For any other language, complete every step marked `required` and run the check in the last step. This page also has a Markdown version at [`/docs/mcp-analytics/installation/other-languages.md`](/docs/mcp-analytics/installation/other-languages.md). + + + +## How it works + +The MCP Analytics dashboards read `$mcp_tool_call` events where `$mcp_source` is `"posthog_mcp_analytics"`. They don't check which library sent the event. To instrument a tool call in your server: + +1. Add a `context` argument to each tool. Agents use it to explain why they called the tool. +2. Remove `context` from the arguments before the tool handler runs. +3. After the handler returns or throws, capture one `$mcp_tool_call` event. + + + + + +Install and configure the [PostHog SDK for your language](/docs/libraries) with your [project token](/docs/feature-flags/installation#where-to-find-your-project-api-key). If your language has no SDK, send events with the [capture API](/docs/api/capture). + +Create one client when the server starts and reuse it. + + + + + +In your `tools/list` response, add a `context` string to each tool's input schema. The agent fills it with the reason for the call. PostHog groups these reasons into the intent clusters on the dashboard. + +```json +{ + "name": "search_docs", + "inputSchema": { + "type": "object", + "properties": { + "query": { "type": "string" }, + "context": { + "type": "string", + "description": "Why are you calling this tool? Describe the goal in general terms, and refer to people by role (\"a customer\") rather than by name. Leave out personal and identifying information." + } + }, + "required": ["query", "context"] + } +} +``` + +Mark `context` as required only if you remove it before your MCP framework validates arguments. Agents fill required arguments more reliably than optional ones. If your framework validates first, make `context` optional so calls without it don't fail. + +Skip any tool that already has an argument named `context`, because that argument belongs to the tool. Also skip any tool whose root schema uses `$ref`, `oneOf`, `anyOf`, or `allOf`. + +When a call arrives, remove `context` from the arguments before the handler runs. If its value is a non-empty string, use it as `$mcp_intent`. See [Capturing agent intent](/docs/mcp-analytics/intent) for how PostHog uses it. + + + + + +Every event needs a non-empty `$session_id`. It groups the calls of one agent conversation, so it must never group calls from different clients. Pick the first case that fits your server: + +| Your server | `$session_id` | +| --- | --- | +| HTTP with an `Mcp-Session-Id` header | `ses_` followed by a hash of the header value, such as the first 32 hex characters of its SHA-256. Use the same hash on every replica. | +| stdio, or another connection that serves only one client | A `ses_` generated when the connection opens. Generate a new one when a tool call arrives 30 minutes or more after the previous call ended, because one stdio process can serve many separate chats. | +| Stateless HTTP with no session header | A fresh `ses_` for each request. | + +Don't use credentials, cookies, or client IP addresses as the session. Different clients can share them. + + + + + +Measure each call with a monotonic clock. Capture one event after the handler returns or throws, and don't block the response while it sends. Your PostHog SDK's capture call is already asynchronous. + +```json +{ + "event": "$mcp_tool_call", + "distinct_id": "ses_9f2c4e0b7d1a4c3e8b6f5a2d1c0e9b8a", + "timestamp": "2026-10-07T14:03:11.204Z", + "properties": { + "$mcp_source": "posthog_mcp_analytics", + "$session_id": "ses_9f2c4e0b7d1a4c3e8b6f5a2d1c0e9b8a", + "$mcp_server_name": "acme-docs", + "$mcp_server_version": "1.4.0", + "$mcp_tool_name": "search_docs", + "$mcp_resource_name": "search_docs", + "$mcp_tool_description": "Search the Acme documentation", + "$mcp_is_error": false, + "$mcp_duration_ms": 182, + "$mcp_intent": "Find how to rotate an API key for a customer", + "$mcp_intent_source": "context_parameter", + "$mcp_client_name": "claude-code", + "$mcp_client_version": "2.1.0", + "$mcp_protocol_version": "2025-11-25", + "$process_person_profile": false + } +} +``` + +Every `$mcp_tool_call` needs these properties. Without them, PostHog can't count, time, or order the call: + +| Property | Value | +| --- | --- | +| `$mcp_source` | Always the string `"posthog_mcp_analytics"`. MCP Analytics ignores events without it. | +| `$mcp_tool_name` | The tool's name, at most 256 characters. | +| `$mcp_is_error` | A JSON boolean, never the string `"false"`. | +| `$mcp_duration_ms` | A JSON number of milliseconds from the start of the request to the handler's result. | +| `$session_id` | From the previous step. | +| `$mcp_server_name` | Your server's name, the same on every event. PostHog uses it to tell your servers apart. | +| `timestamp` | When the request started, not when the event was sent. | + +Send these properties whenever you have the value: + +| Property | Value | +| --- | --- | +| `$mcp_intent`, `$mcp_intent_source` | The `context` value, at most 2,048 characters, with source `"context_parameter"`. Leave both out when the agent sent no context. | +| `$mcp_client_name`, `$mcp_client_version` | The client's `clientInfo` from the request's `_meta` (`io.modelcontextprotocol/clientInfo`), or else from the `initialize` handshake of the same session. | +| `$mcp_protocol_version` | The MCP revision of the request, such as `"2025-11-25"`. | +| `$mcp_client_user_agent`, `$mcp_vendor_client` | The `User-Agent` and `X-Anthropic-Client` headers on HTTP. PostHog uses them to tell clients apart, such as Claude Code from Claude.ai. | +| `$mcp_server_version` | Your server's version. | +| `$mcp_tool_description` | The tool's description, at most 2,048 characters. | +| `$mcp_resource_name` | The tool's name, same as `$mcp_tool_name`. | + +The [event reference](/docs/mcp-analytics/events) lists every property, with types and limits. + +The only nested JSON values are `$mcp_parameters` and `$mcp_response` (see [the payload step](#capture-arguments-and-results)). Keep every other property a string, number, or boolean. + + + + + +If your server knows who is calling, such as from an OAuth token, set `distinct_id` to that user's ID. Send person properties as `$set` and groups as `$groups`, as described in [Identifying users](/docs/mcp-analytics/identifying-users). + +Otherwise, set `distinct_id` to the `$session_id` and set `$process_person_profile` to `false`. That way, anonymous calls don't create person profiles. + + + + + +A call fails when the handler throws, or when it returns a result with `isError: true`. For a failed call, set: + +| Property | Value | +| --- | --- | +| `$mcp_is_error` | `true` | +| `$mcp_error_type` | A low-cardinality label, never the message. Use the thrown error's type name, `"Error"` for an `isError` result, or one of `validation`, `permission`, `missing_context`, `timeout`, `rate_limited`, `api_4xx`, `api_5xx`, `cancelled`, or `internal`. | +| `$mcp_error_message` | The error message, or the text of the `isError` result, at most 2,048 characters after the redaction in the next step. | + +Analytics capture must never change what the client receives. Catch every error from your instrumentation code, and return or rethrow the tool's own failure unchanged. + +To see failures in [Error tracking](/docs/error-tracking) too, capture the exception with your SDK's exception capture, and pass the call's `$session_id` and `$mcp_tool_name`. + + + + + +`$mcp_parameters` and `$mcp_response` let you see what agents sent and what your tool returned. For example, you can find calls that succeed but return an empty or wrong result. They can also contain credentials and personal data, so the SDKs [redact them](/docs/mcp-analytics/privacy) before sending. If you capture them, you need to redact them yourself, before you capture: + +- Send `$mcp_parameters` as `{"request": {"method": "tools/call", "params": {"name": "", "arguments": }}}`, the same shape the SDKs send. +- Replace with `"[redacted]"` the value of every key that matches, ignoring case: `authorization`, `cookie`, `set-cookie`, `x-api-key`, `token`, `password`, `secret`, or `api_key`, `api_token`, `access_token`, `refresh_token`, `client_secret`, or `private_key`, separated by `_`, `-`, or nothing. +- Remove credentials from URLs (`user:password@`, and query fields such as `token` or `signature`). +- Replace `image` and `audio` content blocks, and base64 strings of 10 KB or more, with a short text placeholder. +- Cut strings to 32,768 characters, arrays and objects to 100 entries, and nesting to 10 levels. Keep the whole event under 100 KB. If it's still too large, remove `$mcp_response` first, then `$mcp_parameters`. Never remove a required property. + +Without a redaction pass, leave both properties out. All other features work without them. + + + + + +PostHog SDKs batch events. Flush the client when the server shuts down. On serverless and edge platforms, flush at the end of each request without delaying the response, for example with the platform's `waitUntil`. + + + + + +Call a few tools through an MCP client, flush, then run this query in [SQL insights](https://app.posthog.com/sql): + +```sql +SELECT + count() AS calls, + countIf(properties.$mcp_server_name IS NULL OR properties.$mcp_server_name = '') AS missing_server_name, + countIf(properties.$session_id IS NULL OR properties.$session_id = '') AS missing_session_id, + countIf(JSONType(properties, '$mcp_is_error') != 'Bool') AS bad_is_error, + countIf(JSONType(properties, '$mcp_duration_ms') NOT IN ('Int64', 'UInt64', 'Double')) AS bad_duration, + countIf(properties.$mcp_intent IS NOT NULL) AS with_intent +FROM events +WHERE event = '$mcp_tool_call' + AND properties.$mcp_source = 'posthog_mcp_analytics' + AND timestamp > now() - INTERVAL 1 HOUR +``` + +`calls` should match the number of calls you made, and every `missing_` and `bad_` column should be `0`. `with_intent` counts the calls whose agent sent `context`. If it's `0`, check that `context` appears in your `tools/list` response. + +Then open the [MCP Analytics dashboard](https://app.posthog.com/mcp-analytics/dashboard). Your tools should appear there. + + + + + +## What the SDKs do that this page doesn't + +The SDKs include more features than this page covers. If you need one of these, it's worth checking whether your language has an SDK yet: + +- [Conversation IDs](/docs/mcp-analytics/conversation-id), which group the calls of a stateless HTTP client into one session. +- The [missing capabilities](/docs/mcp-analytics/missing-capability) virtual tool, `get_more_tools`. +- Model capture through the `llm_model` argument. +- `$mcp_tools_list`, `$mcp_initialize`, `$mcp_unknown_tool`, and `$mcp_input_required` events. +- Full redaction, including email addresses, phone numbers, card numbers, and credential-shaped strings anywhere in a payload. + +You can add any of these yourself. The [event reference](/docs/mcp-analytics/events) describes their events and properties. diff --git a/src/navs/index.js b/src/navs/index.js index 76b5a348baaf..1b5dcfc0e92d 100644 --- a/src/navs/index.js +++ b/src/navs/index.js @@ -7611,6 +7611,10 @@ export const docsMenu = { name: 'Ruby', url: '/docs/mcp-analytics/installation/ruby', }, + { + name: 'Other languages', + url: '/docs/mcp-analytics/installation/other-languages', + }, ], }, { From 770139c77e885e3bd9f12872e15bba6f6791ed5c Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 7 Oct 2026 11:11:18 -0300 Subject: [PATCH 2/8] docs(mcp-analytics): redact free text and tighten the hand-rolled contract Pre-PR review (opus + sonnet) found that $mcp_intent and $mcp_error_message went out unredacted on the required path, since the only redaction rules sat in the optional payload step. Adds a required "Redact free text" step and points the payload step at it. Also from the review: - capture only final outcomes of registered tools (no unknown tools, input_required rounds, or task handles) - session table covers SSE and WebSocket and the 2026-07-28 stateless case - $exception keeps the call's distinct_id and $process_person_profile - $mcp_error_type precedence, $mcp_error_status, blank/"{}" intent omitted - optional $mcp_initialize step: the default dashboard template's "MCP clients" and "MCP users and sessions" insights count only that event - check query also flags a missing $mcp_tool_name Tested: re-ran the revised check query on project 2 (124,683 calls in the last hour, all missing_/bad_ counts 0). prettier --check passes. --- .../installation/other-languages.mdx | 75 ++++++++++++++----- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx index f08c08761335..107626f5b793 100644 --- a/contents/docs/mcp-analytics/installation/other-languages.mdx +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -60,7 +60,7 @@ Mark `context` as required only if you remove it before your MCP framework valid Skip any tool that already has an argument named `context`, because that argument belongs to the tool. Also skip any tool whose root schema uses `$ref`, `oneOf`, `anyOf`, or `allOf`. -When a call arrives, remove `context` from the arguments before the handler runs. If its value is a non-empty string, use it as `$mcp_intent`. See [Capturing agent intent](/docs/mcp-analytics/intent) for how PostHog uses it. +When a call arrives, remove `context` from the arguments before the handler runs. If its value is a string that isn't blank or `"{}"`, use it as `$mcp_intent`. See [Capturing agent intent](/docs/mcp-analytics/intent) for how PostHog uses it. @@ -70,18 +70,28 @@ Every event needs a non-empty `$session_id`. It groups the calls of one agent co | Your server | `$session_id` | | --- | --- | -| HTTP with an `Mcp-Session-Id` header | `ses_` followed by a hash of the header value, such as the first 32 hex characters of its SHA-256. Use the same hash on every replica. | -| stdio, or another connection that serves only one client | A `ses_` generated when the connection opens. Generate a new one when a tool call arrives 30 minutes or more after the previous call ended, because one stdio process can serve many separate chats. | -| Stateless HTTP with no session header | A fresh `ses_` for each request. | +| HTTP with a session ID that stays the same across a client's requests, such as the `Mcp-Session-Id` header or an SSE `sessionId` | `ses_` followed by a hash of that ID, such as the first 32 hex characters of its SHA-256. Use the same hash on every replica. | +| stdio, WebSocket, or another connection that serves only one client | A `ses_` generated when the connection opens. Generate a new one when a tool call arrives 30 minutes or more after the previous call ended, because one stdio process can serve many separate chats. | +| HTTP with no session ID, which includes every request on the `2026-07-28` revision | A fresh `ses_` for each request. Each call is then its own session. The SDKs use [conversation IDs](/docs/mcp-analytics/conversation-id) to group these calls. | Don't use credentials, cookies, or client IP addresses as the session. Different clients can share them. +The SDKs derive session IDs with a different hash, so a session served partly by an SDK and partly by your own code splits in two. + Measure each call with a monotonic clock. Capture one event after the handler returns or throws, and don't block the response while it sends. Your PostHog SDK's capture call is already asynchronous. +Capture only the final outcome of a call to a tool your server has. Don't capture a `$mcp_tool_call` for: + +- A call that names a tool your server doesn't have. Its name is whatever the agent sent, so it would add fake tools to your tool list. +- A result with `resultType: "input_required"`. Capture the retry that completes the call instead. +- A task handle (`resultType: "task"`). It isn't a completed call. + +This is the event, as PostHog stores it. If you use the [capture API](/docs/api/capture), wrap it in the request body that page describes, with your project token: + ```json { "event": "$mcp_tool_call", @@ -107,7 +117,7 @@ Measure each call with a monotonic clock. Capture one event after the handler re } ``` -Every `$mcp_tool_call` needs these properties. Without them, PostHog can't count, time, or order the call: +Every `$mcp_tool_call` needs these properties: | Property | Value | | --- | --- | @@ -116,22 +126,22 @@ Every `$mcp_tool_call` needs these properties. Without them, PostHog can't count | `$mcp_is_error` | A JSON boolean, never the string `"false"`. | | `$mcp_duration_ms` | A JSON number of milliseconds from the start of the request to the handler's result. | | `$session_id` | From the previous step. | -| `$mcp_server_name` | Your server's name, the same on every event. PostHog uses it to tell your servers apart. | +| `$mcp_server_name` | Your server's name, at most 256 characters, the same on every event. PostHog uses it to tell your servers apart. | | `timestamp` | When the request started, not when the event was sent. | Send these properties whenever you have the value: | Property | Value | | --- | --- | -| `$mcp_intent`, `$mcp_intent_source` | The `context` value, at most 2,048 characters, with source `"context_parameter"`. Leave both out when the agent sent no context. | +| `$mcp_intent`, `$mcp_intent_source` | The `context` value, [redacted](#redact-free-text) and cut to 2,048 characters, with source `"context_parameter"`. Leave both out when the agent sent no context. | | `$mcp_client_name`, `$mcp_client_version` | The client's `clientInfo` from the request's `_meta` (`io.modelcontextprotocol/clientInfo`), or else from the `initialize` handshake of the same session. | -| `$mcp_protocol_version` | The MCP revision of the request, such as `"2025-11-25"`. | +| `$mcp_protocol_version` | The MCP revision of the request, such as `"2025-11-25"`, from `_meta` (`io.modelcontextprotocol/protocolVersion`), the `MCP-Protocol-Version` header, or the `initialize` handshake. | | `$mcp_client_user_agent`, `$mcp_vendor_client` | The `User-Agent` and `X-Anthropic-Client` headers on HTTP. PostHog uses them to tell clients apart, such as Claude Code from Claude.ai. | | `$mcp_server_version` | Your server's version. | | `$mcp_tool_description` | The tool's description, at most 2,048 characters. | | `$mcp_resource_name` | The tool's name, same as `$mcp_tool_name`. | -The [event reference](/docs/mcp-analytics/events) lists every property, with types and limits. +Cut names, versions, and header values to 256 characters. The [event reference](/docs/mcp-analytics/events) lists every property. The only nested JSON values are `$mcp_parameters` and `$mcp_response` (see [the payload step](#capture-arguments-and-results)). Keep every other property a string, number, or boolean. @@ -152,12 +162,29 @@ A call fails when the handler throws, or when it returns a result with `isError: | Property | Value | | --- | --- | | `$mcp_is_error` | `true` | -| `$mcp_error_type` | A low-cardinality label, never the message. Use the thrown error's type name, `"Error"` for an `isError` result, or one of `validation`, `permission`, `missing_context`, `timeout`, `rate_limited`, `api_4xx`, `api_5xx`, `cancelled`, or `internal`. | -| `$mcp_error_message` | The error message, or the text of the `isError` result, at most 2,048 characters after the redaction in the next step. | +| `$mcp_error_type` | A low-cardinality label, never the message. Use one of `validation`, `permission`, `missing_context`, `timeout`, `rate_limited`, `api_4xx`, `api_5xx`, `cancelled`, `input_required`, or `internal` when one fits. Otherwise use the thrown error's type name, or `"Error"` for an `isError` result. | +| `$mcp_error_message` | The error message, or the text of the `isError` result, [redacted](#redact-free-text) and cut to 2,048 characters. | +| `$mcp_error_status` | Optional. The upstream HTTP status code, as a number, when the failure came from an HTTP call. | Analytics capture must never change what the client receives. Catch every error from your instrumentation code, and return or rethrow the tool's own failure unchanged. -To see failures in [Error tracking](/docs/error-tracking) too, capture the exception with your SDK's exception capture, and pass the call's `$session_id` and `$mcp_tool_name`. +To see failures in [Error tracking](/docs/error-tracking) too, capture the exception with your SDK's exception capture. Use the same `distinct_id` and `$process_person_profile` as the call event, redact the message, and add the call's `$session_id`, `$mcp_tool_name`, and `$exception_source: "mcp.tool_call"`. + + + + + +Agents and errors put credentials and personal data into text. Before you send `$mcp_intent`, `$mcp_error_message`, or an exception message, replace each of these with `[redacted]`: + +- The user and password in a URL (`https://user:password@host`). +- URL query and fragment values whose name is a credential key (below) or a signature, such as `sig`, `signature`, or `X-Amz-Signature`. +- The value after a credential key in `key=value`, `key: value`, or a JSON member such as `"password": "hunter2"`. +- PostHog tokens: `ph`, a lowercase letter, `_`, then 20 or more of `A-Z`, `a-z`, `0-9`, `_`, or `-`. +- Email addresses, phone numbers, IP addresses, card numbers, and US social security numbers. + +A credential key is one of these names, compared as a whole and ignoring case: `authorization`, `cookie`, `set-cookie`, `x-api-key`, `token`, `password`, `secret`, or `api key`, `api token`, `access token`, `refresh token`, `client secret`, or `private key` written with `_`, `-`, or no separator. + +Redact first, then cut the text to its limit. @@ -165,16 +192,25 @@ To see failures in [Error tracking](/docs/error-tracking) too, capture the excep `$mcp_parameters` and `$mcp_response` let you see what agents sent and what your tool returned. For example, you can find calls that succeed but return an empty or wrong result. They can also contain credentials and personal data, so the SDKs [redact them](/docs/mcp-analytics/privacy) before sending. If you capture them, you need to redact them yourself, before you capture: -- Send `$mcp_parameters` as `{"request": {"method": "tools/call", "params": {"name": "", "arguments": }}}`, the same shape the SDKs send. -- Replace with `"[redacted]"` the value of every key that matches, ignoring case: `authorization`, `cookie`, `set-cookie`, `x-api-key`, `token`, `password`, `secret`, or `api_key`, `api_token`, `access_token`, `refresh_token`, `client_secret`, or `private_key`, separated by `_`, `-`, or nothing. -- Remove credentials from URLs (`user:password@`, and query fields such as `token` or `signature`). -- Replace `image` and `audio` content blocks, and base64 strings of 10 KB or more, with a short text placeholder. -- Cut strings to 32,768 characters, arrays and objects to 100 entries, and nesting to 10 levels. Keep the whole event under 100 KB. If it's still too large, remove `$mcp_response` first, then `$mcp_parameters`. Never remove a required property. +- Send `$mcp_parameters` as `{"request": {"method": "tools/call", "params": {"name": "", "arguments": }}}`, the same shape the SDKs send. +- Send `$mcp_response` as the tool's result object, with its `content` blocks and any `structuredContent`. +- Replace with `"[redacted]"` the value of every object key that is a [credential key](#redact-free-text). +- Apply the [free-text redaction](#redact-free-text) to every string value. A text block often holds a JSON copy of `structuredContent`, so it can contain the same credentials. +- Replace `image` and `audio` content blocks, `resource` blocks with a `blob`, and base64 strings of 10 KB or more with a short text placeholder. +- Cut strings to 32,768 characters, arrays and objects to 100 entries, and nesting to 10 levels. + +Keep the whole event under 100 KB. If it's still too large, cut the strings in `$mcp_response` and then `$mcp_parameters` shorter, and remove the property only when that isn't enough. Never remove a required property or `$mcp_error_message`. Without a redaction pass, leave both properties out. All other features work without them. + + +On revisions before `2026-07-28`, a client starts each session with an `initialize` request. Capture a `$mcp_initialize` event for it, with the same `distinct_id`, `$session_id`, and `$process_person_profile` as the session's tool calls, plus `$mcp_source`, `$mcp_client_name`, `$mcp_client_version`, `$mcp_protocol_version`, `$mcp_server_name`, and `$mcp_server_version`. The "MCP clients" and "MCP users and sessions" insights in the MCP Analytics dashboard template count these events. + + + PostHog SDKs batch events. Flush the client when the server shuts down. On serverless and edge platforms, flush at the end of each request without delaying the response, for example with the platform's `waitUntil`. @@ -188,6 +224,7 @@ Call a few tools through an MCP client, flush, then run this query in [SQL insig ```sql SELECT count() AS calls, + countIf(properties.$mcp_tool_name IS NULL OR properties.$mcp_tool_name = '') AS missing_tool_name, countIf(properties.$mcp_server_name IS NULL OR properties.$mcp_server_name = '') AS missing_server_name, countIf(properties.$session_id IS NULL OR properties.$session_id = '') AS missing_session_id, countIf(JSONType(properties, '$mcp_is_error') != 'Bool') AS bad_is_error, @@ -214,7 +251,7 @@ The SDKs include more features than this page covers. If you need one of these, - [Conversation IDs](/docs/mcp-analytics/conversation-id), which group the calls of a stateless HTTP client into one session. - The [missing capabilities](/docs/mcp-analytics/missing-capability) virtual tool, `get_more_tools`. - Model capture through the `llm_model` argument. -- `$mcp_tools_list`, `$mcp_initialize`, `$mcp_unknown_tool`, and `$mcp_input_required` events. -- Full redaction, including email addresses, phone numbers, card numbers, and credential-shaped strings anywhere in a payload. +- `$mcp_tools_list`, `$mcp_unknown_tool`, and `$mcp_input_required` events. +- Wider redaction, such as credential-shaped strings that no key or pattern above identifies. You can add any of these yourself. The [event reference](/docs/mcp-analytics/events) describes their events and properties. From 393fbd73e5fa953c8141b6428327aaff381732d9 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 7 Oct 2026 11:17:57 -0300 Subject: [PATCH 3/8] docs(mcp-analytics): fix Vale warnings on the other-languages page --- .../docs/mcp-analytics/installation/other-languages.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx index 107626f5b793..10b1ba87ccde 100644 --- a/contents/docs/mcp-analytics/installation/other-languages.mdx +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -166,9 +166,9 @@ A call fails when the handler throws, or when it returns a result with `isError: | `$mcp_error_message` | The error message, or the text of the `isError` result, [redacted](#redact-free-text) and cut to 2,048 characters. | | `$mcp_error_status` | Optional. The upstream HTTP status code, as a number, when the failure came from an HTTP call. | -Analytics capture must never change what the client receives. Catch every error from your instrumentation code, and return or rethrow the tool's own failure unchanged. +Analytics capture must never change what the client receives. Catch every error from your instrumentation code, and return or throw the tool's own failure unchanged. -To see failures in [Error tracking](/docs/error-tracking) too, capture the exception with your SDK's exception capture. Use the same `distinct_id` and `$process_person_profile` as the call event, redact the message, and add the call's `$session_id`, `$mcp_tool_name`, and `$exception_source: "mcp.tool_call"`. +To see failures in [Error Tracking](/docs/error-tracking) too, capture the exception with your SDK's exception capture. Use the same `distinct_id` and `$process_person_profile` as the call event, redact the message, and add the call's `$session_id`, `$mcp_tool_name`, and `$exception_source: "mcp.tool_call"`. @@ -176,7 +176,7 @@ To see failures in [Error tracking](/docs/error-tracking) too, capture the excep Agents and errors put credentials and personal data into text. Before you send `$mcp_intent`, `$mcp_error_message`, or an exception message, replace each of these with `[redacted]`: -- The user and password in a URL (`https://user:password@host`). +- The username and password in a URL (`https://user:password@host`). - URL query and fragment values whose name is a credential key (below) or a signature, such as `sig`, `signature`, or `X-Amz-Signature`. - The value after a credential key in `key=value`, `key: value`, or a JSON member such as `"password": "hunter2"`. - PostHog tokens: `ph`, a lowercase letter, `_`, then 20 or more of `A-Z`, `a-z`, `0-9`, `_`, or `-`. From b4131ac3f048e3fb0d59634447240ecc2041b0d5 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 7 Oct 2026 11:37:57 -0300 Subject: [PATCH 4/8] docs(mcp-analytics): shorten the other-languages platform card label The card grid on /docs/mcp-analytics/installation truncated "Other languages" to "Other languag..." in a 1558px window (seen on the deploy preview). The sidebar entry keeps the full name. --- contents/docs/mcp-analytics/installation/other-languages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx index 10b1ba87ccde..e788bbc71eae 100644 --- a/contents/docs/mcp-analytics/installation/other-languages.mdx +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -1,6 +1,6 @@ --- title: Other languages MCP Analytics installation -platformLabel: Other languages +platformLabel: Other platformIconName: IconCode showStepsToc: true --- From 077109d1be8bbaba9105e4c1808166aaf2cf3888 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 27 Oct 2026 19:39:50 -0300 Subject: [PATCH 5/8] docs(mcp-analytics): drop Rust from the languages without an SDK posthog-rs 0.29.0 ships experimental MCP Analytics behind the mcp feature, so a Rust server should use it instead of sending events by hand. The Rust install page is in #21600; link it from here once that merges. --- contents/docs/mcp-analytics/installation/other-languages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx index e788bbc71eae..01f1b050da42 100644 --- a/contents/docs/mcp-analytics/installation/other-languages.mdx +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -7,7 +7,7 @@ showStepsToc: true import { Steps, Step } from 'components/Docs/Steps' -MCP Analytics has SDKs for [TypeScript](/docs/mcp-analytics/installation/typescript), [Python](/docs/mcp-analytics/installation/python), [Go](/docs/mcp-analytics/installation/go), and [Ruby](/docs/mcp-analytics/installation/ruby). If your MCP server uses another language, such as Java, Kotlin, C#, Rust, PHP, Elixir, or Swift, you can still use MCP Analytics. The SDKs send ordinary PostHog events. Send events with the same shape and PostHog shows your server in MCP Analytics. +MCP Analytics has SDKs for [TypeScript](/docs/mcp-analytics/installation/typescript), [Python](/docs/mcp-analytics/installation/python), [Go](/docs/mcp-analytics/installation/go), and [Ruby](/docs/mcp-analytics/installation/ruby). If your MCP server uses another language, such as Java, Kotlin, C#, PHP, Elixir, or Swift, you can still use MCP Analytics. The SDKs send ordinary PostHog events. Send events with the same shape and PostHog shows your server in MCP Analytics. This page describes the event your server sends for each tool call. From 0fb5c33103bb30c36f4548666be089260efc5e6e Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 28 Oct 2026 11:59:05 -0300 Subject: [PATCH 6/8] Cover strict validation and synchronous capture in other-languages guide PostHog Review found two gaps. An optional context argument still fails a validator whose schema sets additionalProperties: false, and the PHP SDK sends its queue inside capture() when the queue is full (QueueConsumer::enqueue, default lib_curl consumer), so capture is not always asynchronous. --- contents/docs/mcp-analytics/installation/other-languages.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx index 01f1b050da42..6d55a7a1dbdf 100644 --- a/contents/docs/mcp-analytics/installation/other-languages.mdx +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -56,7 +56,7 @@ In your `tools/list` response, add a `context` string to each tool's input schem } ``` -Mark `context` as required only if you remove it before your MCP framework validates arguments. Agents fill required arguments more reliably than optional ones. If your framework validates first, make `context` optional so calls without it don't fail. +Remove `context` from the arguments before your MCP framework validates them, if you can. Then you can mark `context` as required, and agents fill required arguments more reliably than optional ones. If your framework validates first, make `context` optional, and add it to the schema that the framework validates against. A schema with `additionalProperties: false` rejects every argument it doesn't list, so calls that include `context` fail. Skip any tool that already has an argument named `context`, because that argument belongs to the tool. Also skip any tool whose root schema uses `$ref`, `oneOf`, `anyOf`, or `allOf`. @@ -82,7 +82,7 @@ The SDKs derive session IDs with a different hash, so a session served partly by -Measure each call with a monotonic clock. Capture one event after the handler returns or throws, and don't block the response while it sends. Your PostHog SDK's capture call is already asynchronous. +Measure each call with a monotonic clock. Capture one event after the handler returns or throws, and don't block the response while it sends. Most PostHog SDKs queue the event and send it in the background. Some send during the capture call. For example, the PHP SDK sends its queue when the queue is full. With such an SDK, capture after the response is sent, such as after `fastcgi_finish_request()` in PHP. Capture only the final outcome of a call to a tool your server has. Don't capture a `$mcp_tool_call` for: From 14bd157a23ce58a5264537e5e0ded985c35ec1a4 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 28 Oct 2026 13:50:16 -0300 Subject: [PATCH 7/8] Link the Rust installation page from the other-languages guide The Rust SDK page lands in #21600; merge that first so the link resolves. --- contents/docs/mcp-analytics/installation/other-languages.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx index 6d55a7a1dbdf..386b7de11d1a 100644 --- a/contents/docs/mcp-analytics/installation/other-languages.mdx +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -7,13 +7,13 @@ showStepsToc: true import { Steps, Step } from 'components/Docs/Steps' -MCP Analytics has SDKs for [TypeScript](/docs/mcp-analytics/installation/typescript), [Python](/docs/mcp-analytics/installation/python), [Go](/docs/mcp-analytics/installation/go), and [Ruby](/docs/mcp-analytics/installation/ruby). If your MCP server uses another language, such as Java, Kotlin, C#, PHP, Elixir, or Swift, you can still use MCP Analytics. The SDKs send ordinary PostHog events. Send events with the same shape and PostHog shows your server in MCP Analytics. +MCP Analytics has SDKs for [TypeScript](/docs/mcp-analytics/installation/typescript), [Python](/docs/mcp-analytics/installation/python), [Go](/docs/mcp-analytics/installation/go), [Ruby](/docs/mcp-analytics/installation/ruby), and [Rust](/docs/mcp-analytics/installation/rust). If your MCP server uses another language, such as Java, Kotlin, C#, PHP, Elixir, or Swift, you can still use MCP Analytics. The SDKs send ordinary PostHog events. Send events with the same shape and PostHog shows your server in MCP Analytics. This page describes the event your server sends for each tool call. -You can follow this page to instrument an MCP server. First check the server's language. For TypeScript, Python, Go, or Ruby, use that language's installation page instead, because the SDK handles redaction and sessions for you. For any other language, complete every step marked `required` and run the check in the last step. This page also has a Markdown version at [`/docs/mcp-analytics/installation/other-languages.md`](/docs/mcp-analytics/installation/other-languages.md). +You can follow this page to instrument an MCP server. First check the server's language. For TypeScript, Python, Go, Ruby, or Rust, use that language's installation page instead, because the SDK handles redaction and sessions for you. For any other language, complete every step marked `required` and run the check in the last step. This page also has a Markdown version at [`/docs/mcp-analytics/installation/other-languages.md`](/docs/mcp-analytics/installation/other-languages.md). From 477d85e3d735bcf0b628e7305fae6999af3a5405 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 28 Oct 2026 13:53:22 -0300 Subject: [PATCH 8/8] Say the PHP SDK sends at its batch size, and route Rust servers to their page PHP's QueueConsumer flushes inside enqueue() at batch_size (100), not when the queue is full (max_queue_size drops). Found by independent verification. --- contents/docs/mcp-analytics/installation/index.mdx | 2 +- contents/docs/mcp-analytics/installation/other-languages.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contents/docs/mcp-analytics/installation/index.mdx b/contents/docs/mcp-analytics/installation/index.mdx index f898c4fdd962..b0ad43044696 100644 --- a/contents/docs/mcp-analytics/installation/index.mdx +++ b/contents/docs/mcp-analytics/installation/index.mdx @@ -12,7 +12,7 @@ The wizard detects your server, installs the SDK, and adds the instrumentation. -For a Go or Ruby server, follow that language's page below. For a language without an SDK, ask your coding agent to follow [`/docs/mcp-analytics/installation/other-languages.md`](/docs/mcp-analytics/installation/other-languages.md). +For a Go, Ruby, or Rust server, follow that language's page below. For a language without an SDK, ask your coding agent to follow [`/docs/mcp-analytics/installation/other-languages.md`](/docs/mcp-analytics/installation/other-languages.md). ## Install by hand diff --git a/contents/docs/mcp-analytics/installation/other-languages.mdx b/contents/docs/mcp-analytics/installation/other-languages.mdx index 386b7de11d1a..62650ec48604 100644 --- a/contents/docs/mcp-analytics/installation/other-languages.mdx +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -82,7 +82,7 @@ The SDKs derive session IDs with a different hash, so a session served partly by -Measure each call with a monotonic clock. Capture one event after the handler returns or throws, and don't block the response while it sends. Most PostHog SDKs queue the event and send it in the background. Some send during the capture call. For example, the PHP SDK sends its queue when the queue is full. With such an SDK, capture after the response is sent, such as after `fastcgi_finish_request()` in PHP. +Measure each call with a monotonic clock. Capture one event after the handler returns or throws, and don't block the response while it sends. Most PostHog SDKs queue the event and send it in the background. Some send during the capture call. For example, the PHP SDK sends its queue when the queue reaches its batch size. With such an SDK, capture after the response is sent, such as after `fastcgi_finish_request()` in PHP. Capture only the final outcome of a call to a tool your server has. Don't capture a `$mcp_tool_call` for: