Custom events and metadata

Sometimes the events the SDK emits out of the box aren't enough. You might want to attach metadata to every event, or emit a fully custom event for something that isn't a tool call. The SDK gives you two hooks for that, in order of increasing invasiveness.

eventProperties — metadata on every event

Pass an eventProperties callback to attach extra properties to every event the SDK emits. The callback runs per request, so values can depend on the current call (request id, transport, headers, env, region, deploy SHA, etc).

TypeScript
const analytics = instrument(server, posthog, {
eventProperties: async (request, extra) => ({
$app_version: process.env.GIT_SHA ?? "unknown",
$mcp_region: process.env.FLY_REGION ?? "unknown",
request_id: extra?.requestInfo?.headers?.["x-request-id"],
}),
})

The returned object is spread flat onto the event's properties alongside the built-in $mcp_* keys:

JSON
{
"event": "$mcp_tool_call",
"properties": {
"$mcp_tool_name": "search_events",
"$app_version": "a1b2c3d",
"$mcp_region": "iad",
"request_id": "req_…",
"…"
}
}

For a "stamp on everything" use case (the closest analogue to posthog.register(...) in other SDKs), just return constants from the callback. The callback is per-event rather than session-persistent, so the values can also vary per request if you need them to.

For group analytics, return groups from your identify callback rather than hand-writing the $groups key — the SDK stamps $groups onto every event for the session for you.

Returned values must be JSON-serializable. Errors thrown from your callback are swallowed and surfaced to your logger — they never interrupt tool execution.

analytics.capture() — emit an arbitrary event

When the built-in events don't cover what you need — for example, recording a feedback signal from your own UI, or capturing a domain event that isn't an MCP request — use the capture() method on the handle returned by instrument(). It writes onto the same queue as everything else, so it inherits the SDK's sanitization, identity, beforeSend, and eventProperties logic. capture() returns a promise you can await.

You name the event. It's sent verbatim — it's your event, so it is not $-prefixed.

TypeScript
const analytics = instrument(server, posthog)
await analytics.capture({
event: "feedback_submitted",
properties: { rating: 5 },
})

What lands in PostHog:

  • One event under the verbatim event name you passed, with your properties merged in.
  • The session id, identity, and any eventProperties callback still apply.

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 eventeventProperties
Emit a one-off event that isn't an MCP requestanalytics.capture()
Attach data to a specific tool call (just that one)Not directly supported — the callbacks run on every event. The SDK doesn't currently expose a per-call hook.

Community questions

Was this page useful?

Questions about this page? or post a community question.