Skip to content

fix(mcp): don't crash stdio servers when rack is missing - #292

Merged
lucasheriques merged 3 commits into
mainfrom
lucas/mcp-optional-rack
Oct 7, 2026
Merged

lucasheriques merged 3 commits into
mainfrom
lucas/mcp-optional-rack

Conversation

@lucasheriques

Copy link
Copy Markdown
Contributor

Why

`PostHog::MCP.instrument` crashed stdio-only MCP servers at boot when `rack` was not installed. The `mcp` gem autoloads its Streamable HTTP transport, and that file raises `LoadError` without rack. `instrument` only rescues `StandardError`, so the error escaped. Reproduced with posthog-ruby 3.26.2 and mcp 1.7.0.

What changed

  • `install_extensions!` now installs the HTTP transport extension in its own method that rescues `LoadError` and logs through `Log.debug`. The server extension is always installed, so stdio capture is unaffected.
  • Adds a patch changeset for `posthog-ruby`.

Scope

  • Covers only the missing-rack boot crash.
  • Does not change how the HTTP transport or `RackMiddleware` behave when rack is present.

Tradeoffs

  • The skip logs at debug, not warn. A stdio server without rack is a valid setup, so a warning on every boot would be noise. HTTP users without rack still get the gem's own `LoadError` when they use the transport.

Blast Radius

Only the boot path of `instrument`. Servers with rack installed take the same path as before.

Verification

  • New spec runs `instrument` plus a stdio `tools/call` in a subprocess where `require 'rack'` raises. It fails on main with the gem's `LoadError` and passes here.
  • `bundle exec rspec`: 1254 examples, 0 failures. `bundle exec rubocop`: clean.

Follow-up: once released, the context-mill `mcp-analytics` skill can drop its `rack` Gemfile workaround and revisit its `posthog-ruby >= 3.26.0` floor.

Failing spec: install_extensions! raises LoadError from the mcp gem's autoloaded StreamableHTTPTransport when rack is absent, crashing stdio servers at boot. Fix follows.
What: PostHog::MCP.instrument installed the Streamable HTTP transport extension unconditionally. The mcp gem autoloads that transport and raises LoadError without rack, which instrument did not rescue (it only rescues StandardError), so stdio-only servers crashed at boot.
Why: rack is only needed for HTTP. A missing rack now skips only the transport extension and logs through Log.debug; the server extension and stdio capture are unaffected.
Tested: bundle exec rspec (1254 examples, 0 failures); bundle exec rubocop clean. New spec fails on the previous commit's code with the LoadError and passes now.
Interrogate gate: both reviewers noted the in-process spec stubbed prepend, not the autoload that raises. Replaced with a subprocess spec that blocks require 'rack' before loading mcp; it fails on main with the gem's LoadError and passes here. Dropped the now-unneeded @extensions_installed reset in reset_for_tests!.
Tested: bundle exec rspec (1254 examples, 0 failures); bundle exec rubocop clean.
@lucasheriques
lucasheriques requested a review from a team as a code owner October 7, 2026 17:23
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

posthog-ruby-async Compliance Report

Date: 2026-10-07T17:25:19.525707+00:00
Duration: 98341ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 110ms
Format Validation.Event Has Uuid ✅ 107ms
Format Validation.Event Has Lib Properties ✅ 111ms
Format Validation.Distinct Id Is String ✅ 155ms
Format Validation.Token Is Present ✅ 108ms
Format Validation.Custom Properties Preserved ✅ 107ms
Format Validation.Event Has Timestamp ✅ 107ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 9ms
Retry Behavior.Retries On 503 ✅ 5312ms
Retry Behavior.Does Not Retry On 400 ✅ 2108ms
Retry Behavior.Does Not Retry On 401 ✅ 2109ms
Retry Behavior.Respects Retry After Header ✅ 8017ms
Retry Behavior.Implements Backoff ✅ 15622ms
Retry Behavior.Retries On 500 ✅ 5212ms
Retry Behavior.Retries On 502 ✅ 5213ms
Retry Behavior.Retries On 504 ✅ 5212ms
Retry Behavior.Max Retries Respected ✅ 15623ms
Deduplication.Generates Unique Uuids ✅ 111ms
Deduplication.Preserves Uuid On Retry ✅ 5212ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10317ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5215ms
Deduplication.No Duplicate Events In Batch ✅ 113ms
Deduplication.Different Events Have Different Uuids ✅ 109ms
Compression.Sends Gzip When Enabled ✅ 107ms
Batch Format.Uses Proper Batch Structure ✅ 106ms
Batch Format.Flush With No Events Sends Nothing ✅ 5ms
Batch Format.Multiple Events Batched Together ✅ 110ms
Error Handling.Does Not Retry On 403 ✅ 2108ms
Error Handling.Does Not Retry On 413 ✅ 2109ms
Error Handling.Retries On 408 ✅ 5212ms

Feature_Flags Tests

⚠️ 16/17 tests passed, 1 failed

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

Failures

request_payload.disable_geoip_omitted_defaults_to_false

Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

posthog-ruby-sync Compliance Report

Date: 2026-10-07T17:25:21.017382+00:00
Duration: 93948ms

⚠️ Some Tests Failed

45/47 tests passed, 2 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 9ms
Format Validation.Event Has Uuid ✅ 5ms
Format Validation.Event Has Lib Properties ✅ 5ms
Format Validation.Distinct Id Is String ✅ 8ms
Format Validation.Token Is Present ✅ 6ms
Format Validation.Custom Properties Preserved ✅ 5ms
Format Validation.Event Has Timestamp ✅ 6ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 5ms
Retry Behavior.Retries On 503 ✅ 5344ms
Retry Behavior.Does Not Retry On 400 ✅ 2011ms
Retry Behavior.Does Not Retry On 401 ✅ 2011ms
Retry Behavior.Respects Retry After Header ✅ 8017ms
Retry Behavior.Implements Backoff ✅ 15297ms
Retry Behavior.Retries On 500 ✅ 5149ms
Retry Behavior.Retries On 502 ✅ 5118ms
Retry Behavior.Retries On 504 ✅ 5145ms
Retry Behavior.Max Retries Respected ✅ 15514ms
Deduplication.Generates Unique Uuids ✅ 20ms
Deduplication.Preserves Uuid On Retry ✅ 5155ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10269ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5126ms
Deduplication.No Duplicate Events In Batch ✅ 22ms
Deduplication.Different Events Have Different Uuids ✅ 11ms
Compression.Sends Gzip When Enabled ✅ 8ms
Batch Format.Uses Proper Batch Structure ✅ 8ms
Batch Format.Flush With No Events Sends Nothing ✅ 5ms
Batch Format.Multiple Events Batched Together ❌ 15ms
Error Handling.Does Not Retry On 403 ✅ 2006ms
Error Handling.Does Not Retry On 413 ✅ 2013ms
Error Handling.Retries On 408 ✅ 5119ms

Failures

batch_format.multiple_events_batched_together

Expected 1 requests, got 5

Feature_Flags Tests

⚠️ 16/17 tests passed, 1 failed

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 ✅ 8ms
Request Payload.Token In Flags Body Matches Init ✅ 10ms
Request Payload.Groups Round Trip ✅ 10ms
Request Payload.Groups Default To Empty Object ✅ 8ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 5ms
Request Payload.Disable Geoip Omitted Defaults To False ❌ 6ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 5ms
Request Lifecycle.No Flags Request On Init Alone ✅ 2ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 5ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 8ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 5ms
Retry Behavior.Retries Flags On 502 ✅ 109ms
Retry Behavior.Retries Flags On 504 ✅ 111ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 7ms

Failures

request_payload.disable_geoip_omitted_defaults_to_false

Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[Medium risk] Handles missing rack dependency in MCP instrumentation.

The PR appears safe to merge; no actionable issues were found.

Reviews (1) · Last reviewed commit: "test(mcp): exercise the real rack LoadEr..." · Reviewed by Greptile

@lucasheriques
lucasheriques merged commit 4970ac8 into main Oct 7, 2026
25 checks passed
@lucasheriques
lucasheriques deleted the lucas/mcp-optional-rack branch October 7, 2026 18:55
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