Skip to content

feat(mcp): add a send_initialize_event option - #1115

Merged
gesh merged 4 commits into
mainfrom
georgis/mcp-capture-initialize-option
Oct 28, 2026
Merged

gesh merged 4 commits into
mainfrom
georgis/mcp-capture-initialize-option

Conversation

@gesh

@gesh gesh commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

💡 Motivation and Context

  • Many MCP clients reconnect before every few tool calls, so $mcp_initialize can outnumber $mcp_tool_call.
  • On PostHog's own MCP server, it was 37% of all MCP events in the last 7 days: 21.1M events, against 17.2M tool calls.
  • Hosts that do not use the event pay for it and cannot turn it off.

Part of one change across three repos: PostHog/posthog-js#5401 (@posthog/mcp) and PostHog/sdk-specs#141 (the spec).

Follow-up, stacked on this PR: #1116 adds the same option for $mcp_tools_list.

Changes

  • New option MCPAnalyticsOptions(send_initialize_event=False). It is on by default, so nothing changes unless a host sets it.
  • The name is not capture_initialize, because PostHogMCP.capture_initialize() is already a method that sends the event by hand.
  • With the option off, record_initialize returns after identify and before the event. The first-request fallback returns early. These are the only places that send the event.
  • Nothing else changes in what the SDK sends. The middleware still mints the session token, so tool calls keep their sessions and client names on every pod. identify still runs in the handshake, so a user known only from initialize stays on later tool calls.
  • In PostHog, MCP onboarding and the users and sessions tiles of the default MCP dashboard read $mcp_initialize. The README says that they show no data while the option is off.
  • README section and a minor changeset.

💚 How did you test it?

  • test_send_initialize_event_off_sends_no_initialize_and_keeps_sessions drives two round-robin pods over HTTP, for the SDK high-level server, standalone fastmcp, and a low-level Server, on both SDK majors. It asserts no $mcp_initialize, a minted token, each connection's tool calls in that token's session, and the client name on every pod.
  • test_send_initialize_event_off_keeps_the_handshake_identity uses an identify callback that returns a user only for initialize. The later tool call keeps that user.
  • test_send_initialize_event_off_also_gates_the_first_request_fallback covers SDK 1.x without HTTP, as on stdio, with the option on and off.
  • The existing tests cover the default.
  • The MCP suites pass on Python 3.10 and 3.14. SDK 1.x has 650 tests, and SDK 2.x has 542.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran sampo add to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Written with Claude Code, model Claude Opus 5.5 (claude-opus-5-5).
  • The option only stops the event. It does not change how the SDK observes the handshake.

🤖 Generated with Claude Code

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 4571911: 1 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment thread posthog/mcp/_instrumentation.py Outdated
MCPAnalyticsOptions(capture_initialize=False) stops $mcp_initialize. The
option is on by default. Many clients reconnect before every few tool
calls, so the handshake event can outnumber $mcp_tool_call.

With the option off, record_initialize and the first-request fallback
return early. Nothing else changes: the middleware still mints the
session token, so tool calls keep their sessions and client names.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@gesh
gesh force-pushed the georgis/mcp-capture-initialize-option branch from 4571911 to dca253f Compare October 23, 2026 13:29
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

posthog-python Compliance Report

Date: 2026-10-28T09:55:24.902541+00:00
Duration: 259914ms

✅ All Tests Passed!

121/121 tests passed


Capture_V1 Tests

✅ 95/95 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 517ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 512ms
Required Headers.Has Authorization Bearer Header ✅ 511ms
Required Headers.Has Content Type Json ✅ 510ms
Required Headers.Has Posthog Sdk Info Format ✅ 511ms
Required Headers.Has Posthog Attempt Header ✅ 510ms
Required Headers.Has Posthog Request Id ✅ 510ms
Required Headers.Has Posthog Request Timestamp ✅ 511ms
Required Headers.Has User Agent ✅ 510ms
Body Format.Body Has Created At And Batch ✅ 511ms
Body Format.No Api Key In Body ✅ 510ms
Body Format.No Sent At In Body ✅ 510ms
Event Format.Event Has Required Root Fields ✅ 510ms
Event Format.Event Uuid Is Valid ✅ 510ms
Event Format.Event Timestamp Is Rfc3339 ✅ 510ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 524ms
Event Format.Distinct Id Is String ✅ 509ms
Event Format.Distinct Id At Root Not Properties ✅ 511ms
Event Format.Custom Properties Preserved ✅ 510ms
Event Format.Set Properties Preserved ✅ 511ms
Event Format.Set Once Properties Preserved ✅ 510ms
Event Format.Groups Properties Preserved ✅ 510ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 510ms
Event Format.Event Has Required Root Fields Batch ✅ 514ms
Event Format.Event Uuid Is Valid Batch ✅ 513ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 514ms
Event Format.Distinct Id Is String Batch ✅ 514ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 513ms
Event Format.Custom Properties Preserved Batch ✅ 514ms
Event Format.Set Properties Preserved Batch ✅ 514ms
Event Format.Set Once Properties Preserved Batch ✅ 513ms
Event Format.Groups Properties Preserved Batch ✅ 514ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 514ms
Batch Behavior.Multiple Events In Single Batch ✅ 518ms
Batch Behavior.Batch Envelope Smoke ✅ 515ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 507ms
Batch Behavior.Flush At Triggers Batch ✅ 1011ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 511ms
Deduplication.Generates Unique Uuids ✅ 518ms
Deduplication.Different Events Same Content Different Uuids ✅ 513ms
Deduplication.Preserves Uuid On Retry ✅ 6520ms
Deduplication.Preserves Timestamp On Retry ✅ 6520ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 6523ms
Deduplication.No Duplicate Events In Batch ✅ 518ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 510ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 13520ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 6516ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 3020ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 6516ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 511ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 515ms
Response Format Validation.Success No Retry After When All Ok ✅ 512ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 1516ms
Response Format Validation.Success No Retry After When Drop Only ✅ 512ms
Response Format Validation.Response Echoes Request Id ✅ 510ms
Retry Behavior.Retries On 408 ✅ 6519ms
Retry Behavior.Retries On 500 ✅ 6515ms
Retry Behavior.Retries On 503 ✅ 8523ms
Retry Behavior.Retries On 504 ✅ 6519ms
Retry Behavior.Retryable Errors Have Retry After ✅ 3517ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 11522ms
Retry Behavior.Does Not Retry On 400 ✅ 2515ms
Retry Behavior.Does Not Retry On 401 ✅ 2514ms
Retry Behavior.Does Not Retry On 402 ✅ 2513ms
Retry Behavior.Does Not Retry On 413 ✅ 2514ms
Retry Behavior.Does Not Retry On 415 ✅ 2514ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 2513ms
Retry Behavior.Implements Backoff ✅ 22536ms
Retry Behavior.Max Retries Respected ✅ 22536ms
Partial Batch Handling.Handles 200 Full Success ✅ 2513ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3517ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3516ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 3517ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 6521ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 6522ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 6523ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 6522ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 6520ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 6521ms
Partial Batch Handling.Respects Retry After On Partial ✅ 8521ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3517ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 3520ms
Compression.Sends Gzip Content Encoding ✅ 510ms
Compression.No Content Encoding When Disabled ✅ 511ms
Compression.Compressed Body Is Decompressible ✅ 510ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 2513ms
Event Options.Cookieless Mode Override ✅ 510ms
Event Options.Disable Skew Correction Override ✅ 510ms
Event Options.Process Person Profile Override ✅ 510ms
Event Options.Product Tour Id Override ✅ 510ms
Event Options.Unset Options Omitted ✅ 510ms
Event Options.Options Override In Batch ✅ 513ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 510ms
Geoip And Historical Migration.Historical Migration Set In Body ✅ 510ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 510ms

Capture_Ai Tests

✅ 5/5 tests passed

View Details
Test Status Duration
Routing.Capture Ai Posts To Ai Endpoint ✅ 511ms
Routing.Capture Does Not Reroute Ai Named Events ✅ 510ms
Identity.Capture Ai Event Has Uuid ✅ 510ms
Identity.Capture Ai Keeps Supplied Uuid ✅ 510ms
Timestamp Format.Non Utc Event Timestamp Is Converted To Utc ✅ 511ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 11ms
Request Payload.Flags Request Uses V2 Query Param ✅ 9ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 9ms
Request Payload.Flags Request Omits Authorization Header ✅ 9ms
Request Payload.Token In Flags Body Matches Init ✅ 9ms
Request Payload.Groups Round Trip ✅ 9ms
Request Payload.Groups Default To Empty Object ✅ 9ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 10ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 9ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 9ms
Request Lifecycle.No Flags Request On Init Alone ✅ 3ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 510ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 14ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 9ms
Retry Behavior.Retries Flags On 502 ✅ 313ms
Retry Behavior.Retries Flags On 504 ✅ 313ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 512ms

Feature_Flags_Local_Evaluation Tests

✅ 4/4 tests passed

View Details
Test Status Duration
Versioned Boolean Matching.Matching Version Missing ✅ 69ms
Versioned Boolean Matching.Matching Version 1 ✅ 67ms
Versioned Boolean Matching.Matching Version 2 ✅ 68ms
Versioned Boolean Matching.Version Only Reload 1 2 1 2 Missing ✅ 35ms

@gesh gesh mentioned this pull request Oct 23, 2026
5 tasks done
record_initialize returned before handle_identify when the option was
off. An identify callback that resolves the user only from the
initialize request then left the session anonymous, so later tool calls
lost the user. Identify now runs first, and the option stops only the
event.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@gesh

gesh commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

@PostHog review

@posthog

posthog Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

@gesh PostHog Review is reviewing this pull request.

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 7e1f2f7: no findings.

@gesh
gesh marked this pull request as ready for review October 23, 2026 14:05
@gesh
gesh requested a review from a team as a code owner October 23, 2026 14:05
@gesh
gesh removed the request for review from a team October 23, 2026 14:06
@gesh gesh added the stamphog Request stamphog review label Oct 23, 2026
@gesh
gesh requested a review from a team October 23, 2026 14:06

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved — this change needs a human reviewer.

Re-adding the stamphog label gives the same result unless the changed files change.

stamphog can't approve this pull request, because two gates refused it. The deny-list gate matched public_api: the change adds capture_initialize to MCPAnalyticsOptions in posthog/mcp/types.py and updates references/public_api_snapshot.txt. The tier gate classified the change as T2-never (108 lines across 6 files, cross-cutting, a feature), so it always needs a human reviewer. The size gate passed, so splitting the PR won't change this outcome. The next step is to ask a human maintainer to review it, ideally one who owns the public API surface of the MCP module.

  • posthog[bot] reviewed the current head.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✗ matches: public_api
size ✓ 14L, 2F substantive, 108L/6F incl. docs/generated/snapshots — within ceiling
tier ✗ classified as T2-never: T2-never (108L, 6F, cross-cutting, feat)
stamphog 2.6.1 .stamphog/policy.yml @ unknown · reviewed head 7e1f2f7

@stamphog stamphog Bot removed the stamphog Request stamphog review label Oct 23, 2026

@lucasheriques lucasheriques left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, approving. One naming question worth settling before merge, plus an optional docs note.

Comment thread posthog/mcp/types.py Outdated
resolve_original_tool: Optional[Callable[[str], Any]] = None
# Send `$mcp_initialize` for each `initialize` handshake. On by default.
# False sends no `$mcp_initialize`. Nothing else changes.
capture_initialize: bool = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These option names collide with the manual capture methods: PostHogMCP.capture_initialize(...) / capture_tools_list(...) already exist. Reading MCPAnalyticsOptions(capture_initialize=False) next to ph.capture_initialize(...) is confusing, and this is public API we'd have to deprecate in two SDKs to change later.

Could we pick names that don't overload the method? A few options:

  • send_initialize_event / send_tools_list_event
  • initialize_event=False / tools_list_event=False
  • one disabled_events=["$mcp_initialize", "$mcp_tools_list"] list, which also covers the next event we want to make optional

Not a strong preference on which one, just want it decided before we ship it. If you'd rather keep the names, fine, but let's do it on purpose. (Same applies to capture_tools_list in #1116, and should match whatever we pick in posthog-js#5401.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Renamed to send_initialize_event in 5af49b1, the same as sendInitializeEvent in posthog-js#5401. I kept a boolean rather than a list of event names, so the config does not depend on event names. Next, capture_tools_list in #1116 becomes send_tools_list_event.

Comment thread posthog/mcp/README.md Outdated
serves one client, so this misses only a client that never sends a request.

Set `capture_initialize=False` to stop sending `$mcp_initialize`. Many clients reconnect before
every few tool calls, so this event can outnumber `$mcp_tool_call`. Nothing else changes: tool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: "Nothing else changes" is true for the SDK, but the dashboard template's users/sessions tiles and the onboarding waiting state both read $mcp_initialize. Maybe "Nothing else changes in what the SDK sends" or a short note? Fine to skip if we're moving those off initialize soon anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 56a712f: "Nothing else changes in what the SDK sends", plus a note that onboarding and the dashboard's users and sessions tiles read $mcp_initialize.

gesh and others added 2 commits October 28, 2026 11:50
PostHogMCP already has a capture_initialize() method that sends the
event by hand. An option with the same name next to that method makes
readers link the two. The new name says what the option controls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
MCP onboarding and the users and sessions tiles of the default MCP
dashboard read $mcp_initialize. The README now says that they show no
data while send_initialize_event is off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@gesh gesh changed the title feat(mcp): add a capture_initialize option feat(mcp): add a send_initialize_event option Oct 28, 2026
@gesh
gesh merged commit d8a1b97 into main Oct 28, 2026
44 checks passed
@gesh
gesh deleted the georgis/mcp-capture-initialize-option branch October 28, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants