From bf529b3c986eab419e452418c5a562dc7e4283b6 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Mon, 22 Jun 2026 18:58:08 -0300 Subject: [PATCH 1/5] docs(mcp-analytics): add Vercel mcp-handler integration example mcp-handler hands you a standard McpServer in its setup callback, so instrument() works with one line. Documents the snippet plus the two Vercel caveats: stateless per-request servers (use a sessionIdGenerator for session continuity) and flushing posthog-node in serverless. Generated-By: PostHog Code Task-Id: b21bc954-5de3-4512-a0d5-6bec2371f782 --- contents/docs/mcp-analytics/installation.mdx | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/contents/docs/mcp-analytics/installation.mdx b/contents/docs/mcp-analytics/installation.mdx index 13b1799c856f..3d22ec0915c0 100644 --- a/contents/docs/mcp-analytics/installation.mdx +++ b/contents/docs/mcp-analytics/installation.mdx @@ -72,6 +72,44 @@ server.tool("search_events", { /* ... */ }, async (args) => { }) ``` +### Next.js / Vercel (`mcp-handler`) + +[`mcp-handler`](https://github.com/vercel/mcp-handler) gives you a standard `McpServer` in its setup +callback, so you instrument it the same way — one line, before or after you register tools: + +```ts +import createMcpHandler from "mcp-handler" +import { PostHog } from "posthog-node" +import { instrument } from "@posthog/mcp" + +// Create the client once at module scope (not per request). +const posthog = new PostHog(process.env.POSTHOG_PROJECT_API_KEY, { + host: "https://us.i.posthog.com", // or https://eu.i.posthog.com +}) + +const handler = createMcpHandler( + (server) => { + instrument(server, posthog) + server.registerTool("roll_dice", { /* ... */ }, async ({ sides }) => { /* ... */ }) + }, + {}, + { basePath: "/api" }, +) + +export { handler as GET, handler as POST } +``` + +Two things to know because of how `mcp-handler` runs on Vercel: + +- **Sessions.** With the default streamable-HTTP transport, `mcp-handler` is stateless — it creates a + fresh server per request and sends no `Mcp-Session-Id`, so each request is its own `$session_id`. + To group a client's calls into one session, run it stateful (configure a `sessionIdGenerator`); the + SDK then derives a stable `$session_id` from the header. Use the [`identify`](/docs/mcp-analytics/identifying-users) + option to attribute calls to a user regardless. +- **Flushing.** `posthog-node` batches events, and a serverless function can freeze before they send. + Flush at the end of the invocation — `await posthog.flush()`, or `ctx.waitUntil(posthog.flush())` to + keep the runtime alive until it completes. + ## Configuration The `posthog` client is passed as the required second positional argument — not in this options object. `instrument()` accepts these options as an optional third argument: From 3cb1501f5a792712e7185e4778e18e56540a7ea8 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 23 Jun 2026 10:16:59 -0300 Subject: [PATCH 2/5] docs(mcp-analytics): show conversation_id grouping for mcp-handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mcp-handler's streamable-HTTP transport is stateless (sessionIdGenerator is typed undefined — no Mcp-Session-Id), so the earlier "configure a sessionIdGenerator" advice was wrong. Replace it with the real lever: enableConversationId, which stitches a client's calls via $mcp_conversation_id without a persistent connection, plus identify for per-user grouping. Generated-By: PostHog Code Task-Id: b21bc954-5de3-4512-a0d5-6bec2371f782 --- contents/docs/mcp-analytics/installation.mdx | 35 ++++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/contents/docs/mcp-analytics/installation.mdx b/contents/docs/mcp-analytics/installation.mdx index 3d22ec0915c0..fa7192773265 100644 --- a/contents/docs/mcp-analytics/installation.mdx +++ b/contents/docs/mcp-analytics/installation.mdx @@ -89,7 +89,11 @@ const posthog = new PostHog(process.env.POSTHOG_PROJECT_API_KEY, { const handler = createMcpHandler( (server) => { - instrument(server, posthog) + instrument(server, posthog, { + // mcp-handler is stateless on Vercel, so group a client's calls with a + // conversation id instead of a transport session (see "Grouping calls" below). + enableConversationId: true, + }) server.registerTool("roll_dice", { /* ... */ }, async ({ sides }) => { /* ... */ }) }, {}, @@ -99,16 +103,27 @@ const handler = createMcpHandler( export { handler as GET, handler as POST } ``` -Two things to know because of how `mcp-handler` runs on Vercel: +#### Grouping a client's calls -- **Sessions.** With the default streamable-HTTP transport, `mcp-handler` is stateless — it creates a - fresh server per request and sends no `Mcp-Session-Id`, so each request is its own `$session_id`. - To group a client's calls into one session, run it stateful (configure a `sessionIdGenerator`); the - SDK then derives a stable `$session_id` from the header. Use the [`identify`](/docs/mcp-analytics/identifying-users) - option to attribute calls to a user regardless. -- **Flushing.** `posthog-node` batches events, and a serverless function can freeze before they send. - Flush at the end of the invocation — `await posthog.flush()`, or `ctx.waitUntil(posthog.flush())` to - keep the runtime alive until it completes. +On Vercel, `mcp-handler`'s streamable-HTTP transport is **stateless**: it spins up a fresh server per +request and issues no `Mcp-Session-Id`, so there's no connection for the SDK to derive a shared +`$session_id` from — left alone, every request lands in its own session. + +Turn on **`enableConversationId`** to stitch the calls together instead. The SDK adds a `conversation_id` +argument to each tool, generates one when the client doesn't pass it, and asks the agent to echo it on +later calls — so a client's tool calls correlate via +[`$mcp_conversation_id`](/docs/mcp-analytics/conversation-id), even with no persistent connection. Group +or break down by that property to see a whole interaction together. + +To also tie the calls to *who* made them, add [`identify`](/docs/mcp-analytics/identifying-users) and +return a `distinctId` from your auth (e.g. the OAuth subject) — that sets `distinct_id` so the calls +group by user as well. + +#### Flushing + +`posthog-node` batches events, and a serverless function can freeze before they send. Flush at the end +of the invocation — `await posthog.flush()`, or `ctx.waitUntil(posthog.flush())` to keep the runtime +alive until it completes. ## Configuration From 284ae2fc56962fdf1c05cf0ffc9501b024d01a11 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 23 Jun 2026 10:37:30 -0300 Subject: [PATCH 3/5] docs(mcp-analytics): fix mcp-handler import (named, not default) mcp-handler exports createMcpHandler as a named export (export { default as createMcpHandler } from "./handler"), per the package entry and README. Was incorrectly shown as a default import. Generated-By: PostHog Code Task-Id: b21bc954-5de3-4512-a0d5-6bec2371f782 --- contents/docs/mcp-analytics/installation.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contents/docs/mcp-analytics/installation.mdx b/contents/docs/mcp-analytics/installation.mdx index fa7192773265..54c0a5b4a66d 100644 --- a/contents/docs/mcp-analytics/installation.mdx +++ b/contents/docs/mcp-analytics/installation.mdx @@ -78,7 +78,7 @@ server.tool("search_events", { /* ... */ }, async (args) => { callback, so you instrument it the same way — one line, before or after you register tools: ```ts -import createMcpHandler from "mcp-handler" +import { createMcpHandler } from "mcp-handler" import { PostHog } from "posthog-node" import { instrument } from "@posthog/mcp" From d2956d3dc0a119d316cacbaeb5af210c03a0a443 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 23 Jun 2026 11:05:53 -0300 Subject: [PATCH 4/5] docs(mcp-analytics): use unified @posthog/mcp import in mcp-handler example @posthog/mcp@0.4.1 re-exports PostHog, so the example imports the client and instrument from one package instead of also importing from posthog-node. Generated-By: PostHog Code Task-Id: b21bc954-5de3-4512-a0d5-6bec2371f782 --- contents/docs/mcp-analytics/installation.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contents/docs/mcp-analytics/installation.mdx b/contents/docs/mcp-analytics/installation.mdx index 54c0a5b4a66d..954aba20431d 100644 --- a/contents/docs/mcp-analytics/installation.mdx +++ b/contents/docs/mcp-analytics/installation.mdx @@ -79,8 +79,7 @@ callback, so you instrument it the same way — one line, before or after you re ```ts import { createMcpHandler } from "mcp-handler" -import { PostHog } from "posthog-node" -import { instrument } from "@posthog/mcp" +import { PostHog, instrument } from "@posthog/mcp" // Create the client once at module scope (not per request). const posthog = new PostHog(process.env.POSTHOG_PROJECT_API_KEY, { From b6463d3b6264b59f79d8bd767ca8eea23a179c59 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 23 Jun 2026 11:32:02 -0300 Subject: [PATCH 5/5] docs(mcp-analytics): beta callout + honest conversation_id grouping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Callout now says beta (pre-1.0), API may change incl. breaking changes in 0.x until v1 — drops the stale hardcoded "0.1.x" (package is 0.4.x). - Grouping: lead with identify (group by user; nothing required from the client) as the robust default. Reframe enableConversationId as best-effort and note its prompt-back is a server instruction in the tool result that some clients may ignore or treat as untrusted (prompt-injection wariness). Generated-By: PostHog Code Task-Id: b21bc954-5de3-4512-a0d5-6bec2371f782 --- contents/docs/mcp-analytics/installation.mdx | 35 +++++++++++--------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/contents/docs/mcp-analytics/installation.mdx b/contents/docs/mcp-analytics/installation.mdx index 954aba20431d..62e3a5c8995d 100644 --- a/contents/docs/mcp-analytics/installation.mdx +++ b/contents/docs/mcp-analytics/installation.mdx @@ -4,9 +4,9 @@ title: Installing the MCP analytics SDK import CalloutBox from 'components/Docs/CalloutBox' - + -`@posthog/mcp` is published as a `0.1.x` alpha. Pin a specific version while we iterate — minor versions may include breaking changes to event shape or option names. A wizard-driven install (`npx @posthog/wizard mcp-analytics add`) is on the roadmap and will replace most of this page once it ships. +`@posthog/mcp` is in beta (pre-1.0). The API may still change — including breaking changes in minor `0.x` releases — until `v1`, so pin a version while we iterate. A wizard-driven install (`npx @posthog/wizard mcp-analytics add`) is on the roadmap and will replace most of this page once it ships. @@ -88,11 +88,7 @@ const posthog = new PostHog(process.env.POSTHOG_PROJECT_API_KEY, { const handler = createMcpHandler( (server) => { - instrument(server, posthog, { - // mcp-handler is stateless on Vercel, so group a client's calls with a - // conversation id instead of a transport session (see "Grouping calls" below). - enableConversationId: true, - }) + instrument(server, posthog) server.registerTool("roll_dice", { /* ... */ }, async ({ sides }) => { /* ... */ }) }, {}, @@ -108,15 +104,24 @@ On Vercel, `mcp-handler`'s streamable-HTTP transport is **stateless**: it spins request and issues no `Mcp-Session-Id`, so there's no connection for the SDK to derive a shared `$session_id` from — left alone, every request lands in its own session. -Turn on **`enableConversationId`** to stitch the calls together instead. The SDK adds a `conversation_id` -argument to each tool, generates one when the client doesn't pass it, and asks the agent to echo it on -later calls — so a client's tool calls correlate via -[`$mcp_conversation_id`](/docs/mcp-analytics/conversation-id), even with no persistent connection. Group -or break down by that property to see a whole interaction together. +The robust way to group is **by user**. Pass [`identify`](/docs/mcp-analytics/identifying-users) and +return a `distinctId` from your auth (e.g. the OAuth subject) — that sets `distinct_id`, so a person's +calls group together no matter how many stateless requests they span, and it requires nothing from the +client: -To also tie the calls to *who* made them, add [`identify`](/docs/mcp-analytics/identifying-users) and -return a `distinctId` from your auth (e.g. the OAuth subject) — that sets `distinct_id` so the calls -group by user as well. +```ts +instrument(server, posthog, { + identify: (request, extra) => ({ distinctId: getUserId(extra) }), +}) +``` + +For finer, per-conversation grouping you can also enable +[`enableConversationId`](/docs/mcp-analytics/conversation-id): the SDK adds a `conversation_id` +argument, generates one when the client doesn't send it, and asks the agent to echo it on later calls, +correlating them via `$mcp_conversation_id`. It's **best-effort** — it works by appending a short +instruction to the tool result, which a cooperative agent echoes but some clients ignore or treat as +untrusted server content (the same wariness they apply to prompt injection). Use it when you control the +client or that trade-off is acceptable; otherwise stick with `identify`. #### Flushing