Go MCP Analytics installation

Contents

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.

posthogmcpsdk instruments servers built on the official Go MCP 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 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. Using a server that isn't built on go-sdk? See Custom dispatchers.

  1. Install the package

    Required
    Terminal
    go get github.com/posthog/posthog-go/posthogmcpsdk

    Earlier releases lack some options and events on this page.

  2. Wrap your server

    Required

    Call posthogmcpsdk.Instrument once, before the server accepts requests:

    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. 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. Check your first events

    Required

    Connect an agent to your server and call a tool. Then open the activity feed and filter for event = $mcp_tool_call. See the event reference 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
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. 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

OptionDefaultWhat it does
WithServerInfo(name, version)–Adds the server name and version to each event.
WithContextParameter(bool)trueAdds a required context argument to capture agent intent. The SDK removes it before your handler and go-sdk's input validation run.
WithCaptureModel(bool)trueCaptures the calling model from client metadata, or from a required llm_model argument the SDK adds.
WithConversationID(bool)trueAdds an optional conversation_id argument to group calls 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.
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)offAdds the get_more_tools virtual tool. 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)trueCaptures the tool arguments as $mcp_parameters.
WithCaptureResponses(bool)trueCaptures the tool result as $mcp_response. A failed call's error text is captured either way.
WithErrorHandler(handler)no-opReceives 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.

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

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?

Was this page useful?