Custom events and metadata
Contents
Use an event properties callback (eventProperties in TypeScript, event_properties in Python and Ruby, WithProperties in Go) to add metadata to captured events. Use analytics.capture() for events that are not MCP requests.
Add metadata to every event
Pass a callback to attach extra properties to automatically captured MCP events. The callback receives request context, such as headers, transport, and the request ID.
In TypeScript, getRequestHeaders reads headers on both MCP SDK majors – see MCP SDK v2.
The returned object is spread flat onto the event's properties alongside the built-in $mcp_* keys:
Return constants from the callback to add the same values to captured MCP events. This is similar to posthog.register(...) in other SDKs. Return values from the request context when metadata must vary between calls.
For group analytics, return groups from identify. The SDK adds $groups to events for the session.
The Go SDK drops returned keys that start with $mcp_ or equal $groups, $set, $process_person_profile, $session_id, or $exception_level.
Returned values must be JSON-serializable. The SDK catches callback errors and sends them to your logger. These errors do not interrupt tool execution.
Send a custom event
Use analytics.capture() for events that aren't MCP requests, such as UI feedback or workflow milestones. It uses the SDK's sanitization, current server session and identity, and beforeSend hook. In TypeScript it returns a promise, and in Python it returns a coroutine, so await it. In Ruby it runs synchronously and returns nil.
Custom capture has no request context and doesn't run the event properties callback. Pass custom metadata in properties.
You name the event. It's sent verbatim – it's your event, so it is not $-prefixed.
The Go SDK has no analytics handle. Call client.Enqueue(posthog.Capture{...}) on your posthog-go client instead. That path has no SDK redaction.
PostHog receives:
- One event under the verbatim
eventname you passed, with yourpropertiesmerged in. - The current server session and cached identity apply. These may differ from the session of a concurrent tool request.
capture() is a method on the handle that instrument() returns, so you call it on the instrumented server's analytics handle directly.
Which one to use
| You want to... | Use |
|---|---|
| Attach the same properties to every auto-captured event | The event properties callback |
| Emit a one-off event that isn't an MCP request | analytics.capture() |
| Attach data only to matching requests | Check the request in the callback and return properties only for matches. |