Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-optional-rack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-ruby': patch
---

Fix `PostHog::MCP.instrument` raising `LoadError` at boot on stdio-only MCP servers that don't have the `rack` gem installed. A missing `rack` now only skips the Streamable HTTP transport extension.
18 changes: 13 additions & 5 deletions lib/posthog/mcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def instrument(server, client = nil, options: nil, **kwargs)
sink = resolved_client ? Sink.new(resolved_client) : nil
data = TrackingData.new(options: opts, sink: sink, server_name: safe_call(server, :name),
server_version: safe_call(server, :version))
install_extensions!
install_extensions!(opts)
server.instance_variable_set(:@__posthog_mcp, data)
register_missing_capability_tool(server, data)
Analytics.new(server)
Expand Down Expand Up @@ -193,15 +193,23 @@ def register_missing_capability_tool(server, data)
Log.warn(data.options, "Warning: could not register the #{name} tool - #{e.class}: #{e.message}")
end

def install_extensions!
def install_extensions!(options)
return if @extensions_installed

::MCP::Server.prepend(ServerExtension)
if defined?(::MCP::Server::Transports::StreamableHTTPTransport)
::MCP::Server::Transports::StreamableHTTPTransport.prepend(TransportExtension)
end
install_transport_extension(options)
@extensions_installed = true
end

# The `mcp` gem autoloads its Streamable HTTP transport and that file raises
# LoadError without `rack`, which a stdio-only server legitimately lacks.
def install_transport_extension(options)
return unless defined?(::MCP::Server::Transports::StreamableHTTPTransport)

::MCP::Server::Transports::StreamableHTTPTransport.prepend(TransportExtension)
rescue LoadError => e
Log.debug(options, "Skipping the Streamable HTTP transport extension - #{e.message}")
end
end
end
end
Expand Down
32 changes: 32 additions & 0 deletions spec/posthog/mcp/instrument_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# rubocop:disable Layout/LineLength

require_relative 'spec_helper'
require 'open3'

class PostHogMcpSpecEchoTool < MCP::Tool
tool_name 'echo'
Expand Down Expand Up @@ -148,6 +149,37 @@ def initialize_request(id = 1, version = '2025-06-18')
handle.capture('custom')
end

it 'instruments a stdio-only server when the rack gem cannot be loaded' do
script = <<~RUBY
module Kernel
alias_method :require_without_rack_block, :require
def require(name)
raise LoadError, 'cannot load such file -- rack' if name == 'rack'

require_without_rack_block(name)
end
end
require 'posthog/mcp'
class EchoTool < MCP::Tool
tool_name 'echo'
input_schema(properties: { message: { type: 'string' } })
def self.call(message: nil, **) = MCP::Tool::Response.new([{ type: 'text', text: message }])
end
client = PostHog::Client.new(api_key: 'phc_test', test_mode: true)
server = MCP::Server.new(name: 'stdio', version: '1.0.0', tools: [EchoTool])
messages = []
handle = PostHog::MCP.instrument(server, client, logger: ->(m) { messages << m })
server.handle({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'echo', arguments: { message: 'hi' } } })
events = []
events << client.dequeue_last_message while client.queued_messages.positive?
puts [handle.class, events.map { |e| e[:event] }.include?('$mcp_tool_call'), messages.grep(/rack/).size].inspect
RUBY

stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', '-e', script)
expect(status.success?).to be(true), stderr
expect(stdout.lines.last).to eq("[PostHog::MCP::Analytics, true, 1]\n")
end

it 'falls back to the posthog-rails facade client when present' do
allow(Kernel).to receive(:warn)
facade_client = PostHog::Client.new(api_key: 'phc_facade', test_mode: true)
Expand Down
Loading