|
| 1 | +"""Stateless / multi-pod MCP analytics with the ``PostHogMCP`` custom dispatcher. |
| 2 | +
|
| 3 | +A stateless MCP server issues no session id, so across pods (or per-request |
| 4 | +transports) ``$session_id`` fragments and the client identity (the "harness", |
| 5 | +e.g. Claude Code / Cursor) -- sent only at ``initialize`` -- is lost on any pod |
| 6 | +that never processed the handshake. |
| 7 | +
|
| 8 | +The fix is a self-encoded session token minted onto the ``Mcp-Session-Id`` |
| 9 | +response header at ``initialize`` and replayed by the client on every request. |
| 10 | +You do NOT set the header by hand: add |
| 11 | +:class:`~posthog.mcp.PostHogMcpStatelessSessionMiddleware` once, then read the |
| 12 | +recovered session with :func:`~posthog.mcp.get_mcp_session` and pass it into the |
| 13 | +capture calls. |
| 14 | +
|
| 15 | +Usage:: |
| 16 | +
|
| 17 | + POSTHOG_PROJECT_API_KEY=phc_xxx uvicorn examples.mcp_stateless_fastapi:app |
| 18 | +
|
| 19 | +The same one-line middleware also works in front of a mounted FastMCP app -- see |
| 20 | +the note at the bottom. |
| 21 | +""" |
| 22 | + |
| 23 | +import os |
| 24 | + |
| 25 | +from fastapi import FastAPI, Request |
| 26 | + |
| 27 | +from posthog.mcp import ( |
| 28 | + PostHogMCP, |
| 29 | + PostHogMcpStatelessSessionMiddleware, |
| 30 | + get_mcp_session, |
| 31 | +) |
| 32 | + |
| 33 | +posthog = PostHogMCP( |
| 34 | + os.environ.get("POSTHOG_PROJECT_API_KEY", "phc_xxx"), |
| 35 | + host=os.environ.get("POSTHOG_HOST", "https://us.i.posthog.com"), |
| 36 | +) |
| 37 | + |
| 38 | +app = FastAPI() |
| 39 | + |
| 40 | +# One line. The middleware mints the session token onto the `Mcp-Session-Id` |
| 41 | +# response header at `initialize` (when the client sent none) and decodes the |
| 42 | +# replayed token on every later request. No manual header handling anywhere. |
| 43 | +app.add_middleware(PostHogMcpStatelessSessionMiddleware) |
| 44 | + |
| 45 | + |
| 46 | +@app.post("/mcp") |
| 47 | +async def mcp_endpoint(request: Request): |
| 48 | + body = await request.json() |
| 49 | + method = body.get("method") |
| 50 | + |
| 51 | + # Recovered by the middleware from the replayed token. On the very first |
| 52 | + # `initialize` it reflects the token just minted; on every later request |
| 53 | + # (any pod) it carries the same session id + harness. |
| 54 | + sess = get_mcp_session(request) |
| 55 | + session_id = sess.session_id if sess else None |
| 56 | + client_name = sess.client_name if sess else None |
| 57 | + client_version = sess.client_version if sess else None |
| 58 | + |
| 59 | + if method == "initialize": |
| 60 | + posthog.capture_initialize( |
| 61 | + session_id=session_id, |
| 62 | + client_name=client_name, |
| 63 | + client_version=client_version, |
| 64 | + parameters=body.get("params"), |
| 65 | + ) |
| 66 | + # ... return your real InitializeResult here ... |
| 67 | + return {"jsonrpc": "2.0", "id": body.get("id"), "result": {}} |
| 68 | + |
| 69 | + if method == "tools/call": |
| 70 | + name = body["params"]["name"] |
| 71 | + prepared = posthog.prepare_tool_call(name, body["params"].get("arguments")) |
| 72 | + # ... dispatch prepared.args to your tool, then: ... |
| 73 | + posthog.capture_tool_call( |
| 74 | + tool_name=name, |
| 75 | + session_id=session_id, |
| 76 | + client_name=client_name, |
| 77 | + client_version=client_version, |
| 78 | + intent=prepared.intent, |
| 79 | + intent_source=prepared.intent_source, |
| 80 | + ) |
| 81 | + return {"jsonrpc": "2.0", "id": body.get("id"), "result": {"content": []}} |
| 82 | + |
| 83 | + return {"jsonrpc": "2.0", "id": body.get("id"), "result": {}} |
| 84 | + |
| 85 | + |
| 86 | +# Mounted-FastMCP variant: the exact same middleware works in front of a FastMCP |
| 87 | +# streamable-HTTP app, and instrument() reads the replayed token automatically -- |
| 88 | +# no per-request code needed there: |
| 89 | +# |
| 90 | +# from mcp.server.fastmcp import FastMCP |
| 91 | +# from posthog import Posthog |
| 92 | +# from posthog.mcp import instrument, PostHogMcpStatelessSessionMiddleware |
| 93 | +# |
| 94 | +# server = FastMCP("my-server", stateless_http=True, json_response=True) |
| 95 | +# instrument(server, Posthog("phc_xxx")) |
| 96 | +# app = server.streamable_http_app() |
| 97 | +# app.add_middleware(PostHogMcpStatelessSessionMiddleware) |
0 commit comments