Other languages MCP Analytics installation

Contents

MCP Analytics has SDKs for TypeScript, Python, Go, Ruby, and 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.

For coding agents

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.

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.
  1. Install a PostHog SDK

    Required

    Install and configure the PostHog SDK for your language with your project token. If your language has no SDK, send events with the capture API.

    Create one client when the server starts and reuse it.

  2. Add the context argument to your tools

    Recommended

    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 for how PostHog uses it.

  3. Resolve a session ID

    Required

    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 sessionIdses_ 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 clientA ses_<UUIDv7> 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 revisionA fresh ses_<UUIDv7> for each request. Each call is then its own session. The SDKs use conversation IDs 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.

  4. Capture a $mcp_tool_call event

    Required

    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, 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:

    PropertyValue
    $mcp_sourceAlways the string "posthog_mcp_analytics". MCP Analytics ignores events without it.
    $mcp_tool_nameThe tool's name, at most 256 characters.
    $mcp_is_errorA JSON boolean, never the string "false".
    $mcp_duration_msA JSON number of milliseconds from the start of the request to the handler's result.
    $session_idFrom the previous step.
    $mcp_server_nameYour server's name, at most 256 characters, the same on every event. PostHog uses it to tell your servers apart.
    timestampWhen the request started, not when the event was sent.

    Send these properties whenever you have the value:

    PropertyValue
    $mcp_intent, $mcp_intent_sourceThe context value, redacted and cut to 2,048 characters, with source "context_parameter". Leave both out when the agent sent no context.
    $mcp_client_name, $mcp_client_versionThe client's clientInfo from the request's _meta (io.modelcontextprotocol/clientInfo), or else from the initialize handshake of the same session.
    $mcp_protocol_versionThe 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_clientThe 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_versionYour server's version.
    $mcp_tool_descriptionThe tool's description, at most 2,048 characters.
    $mcp_resource_nameThe tool's name, same as $mcp_tool_name.

    Cut names, versions, and header values to 256 characters. The event reference lists every property.

    The only nested JSON values are $mcp_parameters and $mcp_response (see the payload step). Keep every other property a string, number, or boolean.

  5. Identify the user

    Optional

    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.

    Otherwise, set distinct_id to the $session_id and set $process_person_profile to false. That way, anonymous calls don't create person profiles.

  6. Record failures

    Required

    A call fails when the handler throws, or when it returns a result with isError: true. For a failed call, set:

    PropertyValue
    $mcp_is_errortrue
    $mcp_error_typeA 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_messageThe error message, or the text of the isError result, redacted and cut to 2,048 characters.
    $mcp_error_statusOptional. 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 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".

  7. Redact free text

    Required

    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.

  8. Capture arguments and results

    Optional

    $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 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": "<tool>", "arguments": <arguments without context, or {}>}}}, 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.
    • Apply the free-text redaction 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.

  9. Capture the handshake

    Recommended

    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.

  10. Flush on shutdown

    Required

    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.

  11. Check your events

    Required

    Call a few tools through an MCP client, flush, then run this query in SQL insights:

    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. 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, which group the calls of a stateless HTTP client into one session.
  • The missing capabilities 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 describes their events and properties.

Still have questions?

Was this page useful?