> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt # Go MCP Analytics installation **Beta SDK** The Go SDK is in beta. It has the same defaults as the other SDKs, but captures fewer events and has fewer optional features. See [Limits](#limits). `posthogmcpsdk` instruments servers built on the official [Go MCP SDK](https://github.com/modelcontextprotocol/go-sdk) (`github.com/modelcontextprotocol/go-sdk`) v1.6.1 or later. Use go-sdk v1.8.0 or later in your own `go.mod` to serve protocol `2026-07-28` and `input_required` rounds. It ships in the [`posthog-go`](/docs/libraries/go.md) repository as its own module, so your app only depends on the MCP SDK if it uses this package. You need Go 1.25+, `posthogmcpsdk` v1.33.0 or later, and a PostHog [project token](/docs/feature-flags/installation.md#where-to-find-your-project-api-key). Using a server that isn't built on go-sdk? See [Custom dispatchers](#custom-dispatchers). 1. 1 ## Install the package Required Terminal ```bash go get github.com/posthog/posthog-go/posthogmcpsdk ``` Earlier releases lack some options and events on this page. 2. 2 ## Wrap your server Required Call `posthogmcpsdk.Instrument` once, before the server accepts requests: Go ```go package main import ( "log" "os" "github.com/modelcontextprotocol/go-sdk/mcp" posthog "github.com/posthog/posthog-go" "github.com/posthog/posthog-go/posthogmcp" "github.com/posthog/posthog-go/posthogmcpsdk" ) func main() { client, err := posthog.NewWithConfig(os.Getenv("POSTHOG_PROJECT_TOKEN"), posthog.Config{ Endpoint: "https://us.i.posthog.com", // or https://eu.i.posthog.com }) if err != nil { log.Fatal(err) } defer client.Close() server := mcp.NewServer(&mcp.Implementation{Name: "weather-server", Version: "1.0.0"}, nil) posthogmcpsdk.Instrument(server, posthogmcp.New(client), posthogmcpsdk.WithServerInfo("weather-server", "1.0.0"), ) // register your tools with mcp.AddTool as usual, then run the server } ``` `WithServerInfo` adds your server's name and version to each event. 3. 3 ## Close the client on shutdown Required `posthog-go` sends events in batches. `client.Close()` sends queued events, so make sure it runs before the process exits. The `defer` above does this when `main` returns. If you stop the process from a signal handler, call `client.Close()` there. 4. 4 ## Check your first events Required Connect an agent to your server and call a tool. Then open the [activity feed](https://app.posthog.com/activity/explore) and filter for `event = $mcp_tool_call`. See [the event reference](/docs/mcp-analytics/events.md) for every property. ## Identify the caller Use `WithIdentity` to attach a person and groups to each call. go-sdk gives you the HTTP headers and any verified OAuth token in `req.Extra`: Go ```go posthogmcpsdk.Instrument(server, posthogmcp.New(client), posthogmcpsdk.WithIdentity(func(ctx context.Context, req *mcp.CallToolRequest) (posthogmcpsdk.Identity, error) { if req.Extra == nil || req.Extra.TokenInfo == nil { return posthogmcpsdk.Identity{}, nil // anonymous } user, err := lookupUser(ctx, req.Extra.TokenInfo.UserID) if err != nil { return posthogmcpsdk.Identity{}, err } return posthogmcpsdk.Identity{ DistinctID: user.ID, Groups: posthog.NewGroups().Set("organization", user.OrgID), SetProperties: posthog.NewProperties().Set("plan", user.Plan), }, nil }), ) ``` An error from the resolver doesn't change the MCP response. The SDK sends the event without identity and reports the error to your `WithErrorHandler`. The Go SDK doesn't send a separate `$identify` event. ## Sessions Every event has a `$session_id`: - **Streamable HTTP with sessions:** the SDK hashes the transport's session ID into `ses_` plus 32 hex characters. Every PostHog MCP SDK computes the same value, so a session served from several languages groups together. - **stdio and in-memory transports:** the SDK generates a `ses_` plus UUIDv7 ID per go-sdk session and starts a new one after 30 minutes without a tool call. - **Stateless HTTP:** there's no transport session, so the SDK uses [conversation IDs](/docs/mcp-analytics/conversation-id.md). It returns a handle in the first result, and calls that echo it share a `$session_id`. A client that ignores the handle gets a new session per request. Use `WithIdentity` to group those calls by user. The SDK mints a handle only when the call has no transport session, the tool's schema can take the argument, the handler returned no Go error, and the result isn't an `input_required` round. stdio and stateful HTTP servers still advertise `conversation_id` but never mint one. A valid conversation handle also takes priority over the transport session, so a conversation that reconnects stays in one session. ## Configuration | Option | Default | What it does | | --- | --- | --- | | `WithServerInfo(name, version)` | – | Adds the server name and version to each event. | | `WithContextParameter(bool)` | `true` | Adds a required `context` argument to capture [agent intent](/docs/mcp-analytics/intent.md). The SDK removes it before your handler and go-sdk's input validation run. | | `WithCaptureModel(bool)` | `true` | Captures the [calling model](/docs/mcp-analytics/events.md#model-capture) from client metadata, or from a required `llm_model` argument the SDK adds. | | `WithConversationID(bool)` | `true` | Adds an optional `conversation_id` argument to [group calls](/docs/mcp-analytics/conversation-id.md) across requests. When it mints a handle, it appends a last `{"conversation_id": ...}` text block to the result. For plain object output schemas, it also mirrors the handle into `_mcp_instructions` in `structuredContent`. | | `WithIdentity(resolver)` | – | Maps a call to a person and groups. See [Identify the caller](#identify-the-caller). | | `WithProperties(resolver)` | – | Adds properties to each `$mcp_tool_call` and its `$exception`. The resolver also receives the result and the handler's error. | | `WithMissingCapabilityTool(name)` | off | Adds the [`get_more_tools` virtual tool](/docs/mcp-analytics/missing-capability.md). Agents call it to report a capability your server lacks. The call sends `$mcp_missing_capability`, not `$mcp_tool_call`. An empty name uses `get_more_tools`. | | `WithCaptureParameters(bool)` | `true` | Captures the tool arguments as `$mcp_parameters`. | | `WithCaptureResponses(bool)` | `true` | Captures the tool result as `$mcp_response`. A failed call's error text is captured either way. | | `WithErrorHandler(handler)` | no-op | Receives instrumentation errors. Its errors and panics never change the MCP response. | To stop the `$exception` event for failed calls, pass `posthogmcp.WithExceptionAutocapture(false)` to `posthogmcp.New`. To change or drop events before they're sent, use `BeforeSend` in your `posthog.Config`. See [Privacy](/docs/mcp-analytics/privacy.md). The adapter's `WithErrorHandler` default is a no-op, but the `posthog-go` client logs to stderr by default. Set `Config.Logger` to change that. The SDK learns each tool's schema from `tools/list` and refreshes it when the server sends `notifications/tools/list_changed`. A server with no connected session sends no notification, so the SDK trusts what it learned for 10 seconds and then lists the tools again on the next call. A tool that declares its own `context`, `llm_model`, or `conversation_id` argument keeps it. Schemas built from `$ref`, `allOf`, `anyOf`, or `oneOf` get no added arguments. Schemas with a non-object type or non-object `properties` are skipped too. Output schemas with `maxProperties` or `propertyNames` get no `_mcp_instructions`. ## Report missing capabilities Add `WithMissingCapabilityTool` to give agents a `get_more_tools` tool. An agent calls it when no tool fits its request. The `context` argument is the agent's report. The option is off by default. Go ```go posthogmcpsdk.Instrument(server, posthogmcp.New(client), posthogmcpsdk.WithServerInfo("weather-server", "1.0.0"), posthogmcpsdk.WithMissingCapabilityTool("get_more_tools"), ) ``` - The SDK adds the tool to the last page of `tools/list`. If your server already registers a tool with that name, the SDK keeps yours. - A call to the tool sends `$mcp_missing_capability` with the agent's request as `$mcp_intent`. It does not send `$mcp_tool_call`. The SDK answers the call, so your handlers never see it. - Pass a different name to rename the tool. An empty name uses `get_more_tools`. - A server that registers no tools must still declare the tools capability. Without it, clients never list tools and never see `get_more_tools`: Go ```go server := mcp.NewServer(&mcp.Implementation{Name: "weather-server", Version: "1.0.0"}, &mcp.ServerOptions{HasTools: true}, ) ``` To see what agents ask for, query the reports: SQL [Run in PostHog](https://us.posthog.com/sql?open_query=SELECT%0A++properties.%24mcp_intent+AS+request%2C%0A++count%28%29+AS+times_asked%0AFROM+events%0AWHERE+event+%3D+'%24mcp_missing_capability'%0AGROUP+BY+request%0AORDER+BY+times_asked+DESC) ```sql SELECT properties.$mcp_intent AS request, count() AS times_asked FROM events WHERE event = '$mcp_missing_capability' GROUP BY request ORDER BY times_asked DESC ``` See [Tracking missing capabilities](/docs/mcp-analytics/missing-capability.md) for how to read the reports. ## Middleware order `Instrument` adds receiving and sending middleware. Receiving middleware you add before `Instrument` runs inside the measured duration. Middleware you add after it runs outside. To place the middleware yourself, install both halves of `NewMiddleware`: Go ```go middleware := posthogmcpsdk.NewMiddleware(posthogmcp.New(client)) server.AddReceivingMiddleware(middleware.Receiving) server.AddSendingMiddleware(middleware.Sending) ``` ## Custom dispatchers For a server that isn't built on go-sdk, use the `posthogmcp` package directly. `CaptureToolCall` builds the same `$mcp_tool_call` event, with the same redaction and size limits: Go ```go analytics := posthogmcp.New(client) err := analytics.CaptureToolCall(ctx, posthogmcp.ToolCall{ ToolName: "search_docs", Intent: intent, // the agent's "context" argument, if you add one IntentSource: posthogmcp.IntentSourceContextParameter, Parameters: arguments, Response: result, Duration: time.Since(started), Error: toolErr, // sets $mcp_is_error and emits $exception DistinctID: user.ID, SessionID: sessionID, ClientName: clientName, ClientVersion: clientVersion, ProtocolVersion: protocolVersion, }) if err != nil { log.Printf("capture MCP analytics: %v", err) } ``` A call with neither `SessionID` nor `ConversationID` gets a new `ses_` plus UUIDv7 session, and `distinct_id` falls back to it. Pass a stable `SessionID` or `ConversationID` to group calls. `ToolCall` also takes `ConversationID`, `LLMModel` with `LLMModelSource`, `ClientUserAgent`, and `VendorClient`. This path has no prepare steps. You strip any injected arguments and manage sessions and conversation handles yourself. If `Intent` comes from an argument, remove it from `Parameters`. An `Intent` of `{}` is omitted. `SetProperties` becomes `$set` only with an explicit `DistinctID`. Two more methods capture calls that are not tool calls. See [Calls that are not tool calls](#calls-that-are-not-tool-calls): Go ```go err = analytics.CaptureUnknownTool(ctx, posthogmcp.UnknownTool{ EventContext: posthogmcp.EventContext{SessionID: sessionID}, ToolName: requestedName, }) err = analytics.CaptureInputRequired(ctx, posthogmcp.InputRequired{ EventContext: posthogmcp.EventContext{SessionID: sessionID}, ToolName: "search_docs", Duration: time.Since(started), Methods: []string{"elicitation/create"}, // the method of each request, never its content }) ``` ## Events The Go SDK sends `$mcp_tool_call`, `$exception` for failed calls, `$mcp_unknown_tool`, `$mcp_input_required`, and, when you turn it on, `$mcp_missing_capability`. `WithIdentity` applies to all of them. `WithProperties` applies to `$mcp_tool_call` and its `$exception`. ### Calls that are not tool calls These `tools/call` requests don't fire `$mcp_tool_call`: - **Unknown tool.** A call that names a tool the server hasn't registered sends `$mcp_unknown_tool`. It carries the requested name, redacted, and a conversation ID only if the agent sent a valid handle. The SDK never mints a handle for it. A blank name sends nothing. - **`input_required` round.** On `2026-07-28`, a tool can end a round by asking the client for input. The SDK sends `$mcp_input_required` with the round's duration and `$mcp_input_request_methods`, the method of each request, at most 100. It never captures the requests or the answers. The retry that completes the call sends the `$mcp_tool_call`. - **Missing-capability report.** When the agent calls the `get_more_tools` virtual tool, the SDK answers the call itself and sends `$mcp_missing_capability`. The agent's report is `$mcp_intent`. The tool name is `$mcp_resource_name`. The event never carries `$mcp_parameters`. Your handlers never see the call. A client on an older revision never sees a round. go-sdk answers the requests itself and runs the handler once more. If the handler asks again, the call is a failed `$mcp_tool_call` with `$mcp_error_type` set to `input_required`. ### Properties - **`$lib`:** MCP events report `posthog-go-mcp`. `$lib_version` stays the `posthog-go` version. A client built with `CaptureMode: posthog.CaptureModeAnalyticsV1` still reports `posthog-go` for MCP events. - **Errors:** handler errors from `mcp.AddTool` keep their Go type in `$mcp_error_type` and `$exception`, such as `fs.PathError`. Plain errors give `Error`. The adapter has no override. On the manual API, set `ToolCall.ErrorType`. - **Client headers:** `$mcp_client_user_agent` and `$mcp_vendor_client` come from the `User-Agent` and `X-Anthropic-Client` headers, on HTTP only. The SDK redacts them and caps each at 256 bytes. ## Limits The Go SDK doesn't yet have: - An intent fallback callback. With `WithContextParameter(false)`, the SDK captures no intent - `$mcp_tools_list`, `$mcp_initialize`, resource, and prompt events - Task handling - Client name and version on legacy `2025-11-25` stateless servers. The protocol version is still present - `$identify` and the merge of anonymous events into a user - Wizard support Some behavior to know about: - A before-send hook that drops a tool call leaves its `$exception`. - On a stateless server with no session, an `input_required` round and its retry land in different sessions. - The `$exception` has no stack trace, and no user agent, vendor, or model properties. ### Still have questions? Ask PostHog AI ### Was this page useful? HelpfulCould be better