diff --git a/contents/docs/mcp-analytics/events.mdx b/contents/docs/mcp-analytics/events.mdx index 3022e794bc50..4a32716c3630 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 cb39308e4627..b0ad43044696 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, 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 Choose your server's language. For tools that a web page registers, choose WebMCP: 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..62650ec48604 --- /dev/null +++ b/contents/docs/mcp-analytics/installation/other-languages.mdx @@ -0,0 +1,257 @@ +--- +title: Other languages MCP Analytics installation +platformLabel: Other +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), [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, 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). + + + +## 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"] + } +} +``` + +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`. + +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. + + + + + +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 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. 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: + +- 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", + "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: + +| 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, 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, [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"`, 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`. | + +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. + + + + + +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 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 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"`. + + + + + +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 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 `-`. +- 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. + + + + + +`$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. +- 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`. + + + + + +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_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, + 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_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. diff --git a/src/navs/index.js b/src/navs/index.js index a057e46a3743..aaa3dea1f6ae 100644 --- a/src/navs/index.js +++ b/src/navs/index.js @@ -7627,6 +7627,10 @@ export const docsMenu = { name: 'WebMCP', url: '/docs/mcp-analytics/installation/webmcp', }, + { + name: 'Other languages', + url: '/docs/mcp-analytics/installation/other-languages', + }, ], }, {