
Security News
White House Authorizes Private Companies to Conduct Offensive Cyber Operations
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.
@posthog/mcp
Advanced tools
PostHog SDK for Model Context Protocol (MCP) servers — tracks tool usage, intent, and identity
Please see the main PostHog docs.
SDK usage examples and code snippets live in the official documentation so they stay up to date.
On stateless deployments the SDK mints the Mcp-Session-Id response header at initialize
as a token carrying the session id and client name/version. Clients replay the header on
every request, so any pod keeps $session_id and $mcp_client_name/$mcp_client_version
stable with no server-side store.
A standalone $identify event fires at most once per session — at initialize (or, on a
long-lived server, when the identity first appears or materially changes). Tool calls on other
pods reuse the identity to stamp distinct_id/$set on every event without re-publishing
$identify, so person properties are never lost. (Edge case: if identity isn't resolvable
until after initialize, the first $identify is suppressed too, so pre-identify anonymous
events aren't aliased onto the user — see docs/ARCHITECTURE.md §4.) To drop $identify
entirely, return null from beforeSend when event === '$identify'.
enableJsonResponse: trueThe token is minted onto the Mcp-Session-Id response header from inside the initialize
handler, so it only reaches the client when the transport builds the response after the
handler runs — i.e. JSON mode. In SSE (streaming) mode StreamableHTTPServerTransport
flushes the response headers before the handler runs, so the minted header never lands and
behavior silently falls back to a session-per-request. This is a property of the transport, so
it applies to every Streamable-HTTP host — set enableJsonResponse: true (and use a fresh
transport per request):
// @modelcontextprotocol/sdk
new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true })
// Cloudflare agents / createMcpHandler (SSE is the default)
createMcpHandler(server, { enableJsonResponse: true })
// @rekog/mcp-nest
McpModule.forRoot({ streamableHttp: { enableJsonResponse: true } })
Set the header yourself at the HTTP layer with the exported encodeSessionId (read clientInfo
from the initialize body) — the SDK decodes it either way:
import { MCP_SESSION_HEADER, encodeSessionId, newSessionId } from '@posthog/mcp'
// after parsing the POST body, before flushing headers:
if (body?.method === 'initialize' && !req.headers[MCP_SESSION_HEADER]) {
res.setHeader(
MCP_SESSION_HEADER,
encodeSessionId({
sessionId: newSessionId(),
clientName: body.params?.clientInfo?.name,
clientVersion: body.params?.clientInfo?.version,
})
)
}
Details: docs/ARCHITECTURE.md §4.
instrument() works on both SDK majors — @modelcontextprotocol/sdk v1 and
@modelcontextprotocol/{core,server,client} v2 — and on both the high-level McpServer and the
low-level Server. Shapes are detected at runtime, so neither major is a dependency here.
Protocol revision is a property of each request, not of the server: a v2 server serves both
2025-11-25 and 2026-07-28 traffic, and the SDK is instrumented once for both.
On 2026-07-28 there is no initialize and no session header — the revision removed
protocol-level sessions, and this SDK will not mint one. Session correlation therefore comes from
enableConversationId, which is opt-in. Without it every request is its own $session_id.
On 2025-11-25, the session id and the client's name and version are exchanged once at
initialize. If your server builds a fresh McpServer per HTTP request — which
createMcpHandler does by default — the instance serving a later tools/call never saw that
handshake. The SDK bridges it by minting the Mcp-Session-Id token described above, which the
client replays on every request.
That token only reaches the client if the transport builds response headers after the handler
runs. @rekog/mcp-nest with enableJsonResponse: true does; createMcpHandler's legacy leg
does not, and there is no setting we can reach from inside the server. On that leg, expect
$mcp_client_name and $mcp_client_version to be absent for 2025-11-25 traffic — the protocol
version still arrives, because clients send it on the MCP-Protocol-Version header of every
request.
If your identify, intentFallback, eventProperties or beforeSend reads HTTP headers, it has
to change. The two majors put the request in different places and in different shapes: v1 attaches
a plain object at extra.requestInfo.headers, v2 attaches the WHATWG Request at extra.http.req,
whose headers only answers to .get(). A v1-shaped read returns undefined on v2 — an
identify() written that way returns null and every event goes anonymous.
The SDK hands your callback whatever the MCP SDK handed it, unchanged; it does not fake a v1
shape on v2, because a partially synthesised requestInfo is a more convincing lie than an absent
one. Read headers through the exported helper instead, which handles both majors, lowercases keys,
and duck-types Headers so it also works on edge runtimes:
import { getRequestHeaders } from '@posthog/mcp'
identify: async (request, extra) => {
const auth = getRequestHeaders(extra)?.['authorization'] // v1 and v2
// ...
}
$mcp_intent records, and how to turn it offcontext defaults to on: the SDK adds a context parameter to every tool it advertises, asks
the agent to say why it is calling, and records the answer as $mcp_intent. It is stripped before
your tool runs wherever the SDK can confirm the parameter is its own.
On a server that builds a fresh instance per HTTP request — createMcpHandler, or @rekog/mcp-nest
in its stateless mode — that confirmation is not available: ownership is learned while serving
tools/list, and the instance handling a tools/call never served one. There the SDK records the
argument but does not strip it, because deleting an argument your tool declared would cost you
the call, while an extra key usually costs nothing.
What matters is instance lifetime, not statelessness. A server that is stateless at the transport
(sessionIdGenerator: undefined) but keeps one long-lived server object learns ownership from the
first tools/list and keeps it, so none of the above applies to it.
The consequence worth knowing: if your own tool declares a parameter named context and the SDK
cannot tell that it is yours, its value is recorded as $mcp_intent. It never leaves your project,
and it is capped at 2048 characters. Two ways out, both one line:
instrument(server, posthog, { context: false }) // no injection, no capture
instrument(server, posthog, { // keep it, drop the property
beforeSend: (event) => {
delete event.properties.$mcp_intent
return event
},
})
intentFallback is the third option: supply the intent yourself when the agent did not send one.
instrument(server.server)Before v2 support landed, the compatibility gate rejected high-level v2 servers, and the usual
workaround was to instrument the underlying low-level server. instrument(server) now works, so
you can go back to the documented call.
Whether the workaround costs you anything depends on your stack: instrumenting the low-level server
skips the high-level tool registry, so tool descriptions and callback-level wrapping come only from
what is advertised over tools/list. If your framework never calls registerTool() — @rekog/mcp-nest
does not — the registry is empty and the two calls behave the same.
To test local changes in a consumer app (e.g. a dummy MCP server), symlink both
@posthog/mcp and its posthog-node peer from this monorepo into the app — run from the
app's directory:
mkdir -p node_modules/@posthog # in case the app has no other @posthog/-scoped deps yet
ln -s /absolute/path/to/posthog-js/packages/mcp node_modules/@posthog/mcp
ln -s /absolute/path/to/posthog-js/packages/node node_modules/posthog-node
Then keep a watch build running and restart the app after each change:
cd /absolute/path/to/posthog-js/packages/mcp && pnpm dev # rebuilds dist/ on save
# in the app (e.g. dummy mcp), after each rebuild:
npm start # Node caches dist/ at startup, so restart to pick it up
npm install in the app replaces both symlinks with published copies — re-create them if you run it.cd packages/mcp && pnpm test:unit
FAQs
PostHog SDK for Model Context Protocol (MCP) servers — tracks tool usage, intent, and identity
The npm package @posthog/mcp receives a total of 46,578 weekly downloads. As such, @posthog/mcp popularity was classified as popular.
We found that @posthog/mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 21 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.

Research
/Security News
The campaign amassed more than 75,000 installs by targeting Russian-speaking users seeking access to blocked services.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.