From e67076c1bcfc45a70b73f5ef2283c15d2171b911 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 27 Oct 2026 19:42:23 -0300 Subject: [PATCH 1/6] feat(mcp): pass every HTTP request header to MCP callbacks identify, intent_fallback and event_properties read extra['headers'], but RequestScope only copied four headers into it (user-agent, x-anthropic-client, mcp-session-id, mcp-protocol-version). So identify-by-bearer-token, the pattern the TS (getRequestHeaders) and Python (get_request_headers) docs show, always saw nil in Ruby. headers_from_env now maps every HTTP_* Rack env key to its lowercase header name, matching the other SDKs. No header value is captured. Identity#captured_extra keeps only scalars from extra (the headers Hash is dropped from $identify), TransportIdentity stamps user-agent and x-anthropic-client alone, and key-based sanitization redacts authorization / x-api-key / cookie as a second layer. Tested: new parameterized spec in http_transport_spec drives a real StreamableHTTPTransport with `authorization` and `x-tenant-id`. identify resolves the user from the header, and no captured event's JSON contains the value. Red before the change ("anonymous"). Mutation check: letting captured_extra keep Hash values fails the x-tenant-id case (authorization is still caught by redaction). `bundle exec rspec`: 1896 examples, 0 failures, 40 pending (pre-existing gem-gated). rubocop clean on touched files. --- .changeset/mcp-request-headers.md | 5 +++++ lib/posthog/mcp/request_scope.rb | 29 ++++++++++++------------- spec/posthog/mcp/http_transport_spec.rb | 17 +++++++++++++++ 3 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 .changeset/mcp-request-headers.md diff --git a/.changeset/mcp-request-headers.md b/.changeset/mcp-request-headers.md new file mode 100644 index 00000000..5d2f12e7 --- /dev/null +++ b/.changeset/mcp-request-headers.md @@ -0,0 +1,5 @@ +--- +"posthog-ruby": minor +--- + +MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names), so a callback can resolve a user from an `Authorization` bearer token or an API key. Header values are never captured on events. diff --git a/lib/posthog/mcp/request_scope.rb b/lib/posthog/mcp/request_scope.rb index b39281a6..48ba22e4 100644 --- a/lib/posthog/mcp/request_scope.rb +++ b/lib/posthog/mcp/request_scope.rb @@ -17,14 +17,6 @@ module RequestScope KEY = :posthog_mcp_request_scope FIBER_STORAGE = Fiber.respond_to?(:[]) && Fiber.respond_to?(:[]=) - # Rack `env` keys for the headers the integration reads, by header name. - HEADER_ENV_KEYS = { - 'user-agent' => 'HTTP_USER_AGENT', - 'x-anthropic-client' => 'HTTP_X_ANTHROPIC_CLIENT', - 'mcp-session-id' => 'HTTP_MCP_SESSION_ID', - 'mcp-protocol-version' => 'HTTP_MCP_PROTOCOL_VERSION' - }.freeze - module_function # @return [Hash, nil] `{headers:, transport:, mint:, session_id:}` for the in-flight HTTP request, @@ -46,20 +38,27 @@ def current=(value) end end - # @param headers [Hash{String => String}] lowercase header names - # @param transport [Symbol] `:http` for the Streamable HTTP transport, `:other` - # for a transport that publishes no headers (stdio, a custom dispatcher) + # Every request header, so `identify`, `intent_fallback` and `event_properties` + # can read the ones they need (an `authorization` bearer token, an API key) + # from `extra['headers']`. No header value is captured: {Identity} keeps only + # scalars from `extra`, and {TransportIdentity} stamps `user-agent` and + # `x-anthropic-client` alone. + # # @param env [Hash] a Rack `env` - # @return [Hash{String => String}] the headers {Instrumentation} reads, lowercase + # @return [Hash{String => String}] lowercase header names def headers_from_env(env) return {} unless env.is_a?(Hash) - HEADER_ENV_KEYS.each_with_object({}) do |(name, env_key), acc| - value = env[env_key] - acc[name] = value if value.is_a?(String) && !value.empty? + env.each_with_object({}) do |(key, value), acc| + next unless key.is_a?(String) && key.start_with?('HTTP_') && value.is_a?(String) && !value.empty? + + acc[key.delete_prefix('HTTP_').downcase.tr('_', '-')] = value end end + # @param headers [Hash{String => String}] lowercase header names + # @param transport [Symbol] `:http` for the Streamable HTTP transport, `:other` + # for a transport that publishes no headers (stdio, a custom dispatcher) def with(headers:, transport: :http) previous = current self.current = { headers: headers, transport: transport, mint: nil, session_id: nil } diff --git a/spec/posthog/mcp/http_transport_spec.rb b/spec/posthog/mcp/http_transport_spec.rb index aa8878f1..6eeb59f8 100644 --- a/spec/posthog/mcp/http_transport_spec.rb +++ b/spec/posthog/mcp/http_transport_spec.rb @@ -95,6 +95,23 @@ def parse(response) expect(logs.count { |m| m.include?('fragment across requests') }).to eq(1) end + { + 'authorization' => 'Bearer secret-token-should-not-leak', + 'x-tenant-id' => 'tenant-should-not-leak' + }.each do |header, secret| + it "hands identify the #{header} header without capturing it on any event" do + identify = ->(_request, extra) { { distinct_id: extra['headers'][header] == secret ? 'user-1' : 'anonymous' } } + PostHog::MCP.instrument(server, client, identify: identify, enable_conversation_id: false) + token = transport.call(env_for(initialize_body, header => secret))[1]['mcp-session-id'] + transport.call(env_for(rpc(2, 'tools/call', { name: 'ping_tool', arguments: {} }), 'mcp-session-id' => token, header => secret)) + + events = drain_events(client) + expect(events_named(events, '$mcp_tool_call').map { |e| e[:distinct_id] }).to eq(['user-1']) + expect(events_named(events, '$identify').size).to eq(1) + expect(events.map { |e| JSON.generate(e) }.grep(/#{secret}/)).to eq([]) + end + end + it 'leaves uninstrumented servers alone' do response = transport.call(env_for(initialize_body)) expect(response[0]).to eq(200) From 1efeaaf387b251ba792dd93d12f179eb4ffc9b63 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 27 Oct 2026 19:45:16 -0300 Subject: [PATCH 2/6] fix(mcp): include unprefixed headers, drop HTTP_VERSION, freeze the bag Pre-PR gate fixes (opus + sonnet interrogate, both raised each): - Rack stores Content-Type/Content-Length as CONTENT_TYPE/CONTENT_LENGTH, so they were missing; TS and Python expose them. - HTTP_VERSION is the request line's protocol on WEBrick/Thin, not a header. - Freeze the Hash: callbacks share it with session/protocol resolution. - Changeset and docstring now say the guarantee covers SDK capture paths, not raw values a callback returns. Tested: new parameterized request_scope_spec for headers_from_env. Full bundle exec rspec green. --- .changeset/mcp-request-headers.md | 2 +- lib/posthog/mcp/request_scope.rb | 29 +++++++++++++++++++------- spec/posthog/mcp/request_scope_spec.rb | 27 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 spec/posthog/mcp/request_scope_spec.rb diff --git a/.changeset/mcp-request-headers.md b/.changeset/mcp-request-headers.md index 5d2f12e7..b3907ad0 100644 --- a/.changeset/mcp-request-headers.md +++ b/.changeset/mcp-request-headers.md @@ -2,4 +2,4 @@ "posthog-ruby": minor --- -MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names), so a callback can resolve a user from an `Authorization` bearer token or an API key. Header values are never captured on events. +MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names, frozen), so a callback can resolve a user from an `Authorization` bearer token or an API key. The SDK never captures header values. Resolve a header to an id instead of returning its raw value in identity properties or `event_properties`, which are sent unredacted. diff --git a/lib/posthog/mcp/request_scope.rb b/lib/posthog/mcp/request_scope.rb index 48ba22e4..2f35c704 100644 --- a/lib/posthog/mcp/request_scope.rb +++ b/lib/posthog/mcp/request_scope.rb @@ -38,22 +38,35 @@ def current=(value) end end + # Rack `env` keys for the two request headers Rack stores without `HTTP_`. + UNPREFIXED_HEADER_ENV_KEYS = { 'CONTENT_TYPE' => 'content-type', 'CONTENT_LENGTH' => 'content-length' }.freeze + # The request line's protocol, which some servers (WEBrick, Thin) put in `env`. + PROTOCOL_ENV_KEY = 'HTTP_VERSION' + # Every request header, so `identify`, `intent_fallback` and `event_properties` # can read the ones they need (an `authorization` bearer token, an API key) - # from `extra['headers']`. No header value is captured: {Identity} keeps only - # scalars from `extra`, and {TransportIdentity} stamps `user-agent` and - # `x-anthropic-client` alone. + # from `extra['headers']`. The SDK captures no header value: {Identity} keeps + # only scalars from `extra`, and {TransportIdentity} stamps `user-agent` and + # `x-anthropic-client` alone. A callback that returns a raw header value in + # identity properties or `event_properties` sends it, unredacted. # # @param env [Hash] a Rack `env` - # @return [Hash{String => String}] lowercase header names + # @return [Hash{String => String}] lowercase header names, frozen because + # {Instrumentation} reads session and protocol headers from it after callbacks run def headers_from_env(env) - return {} unless env.is_a?(Hash) + return {}.freeze unless env.is_a?(Hash) env.each_with_object({}) do |(key, value), acc| - next unless key.is_a?(String) && key.start_with?('HTTP_') && value.is_a?(String) && !value.empty? + name = header_name(key) + acc[name] = value if name && value.is_a?(String) && !value.empty? + end.freeze + end - acc[key.delete_prefix('HTTP_').downcase.tr('_', '-')] = value - end + def header_name(env_key) + return UNPREFIXED_HEADER_ENV_KEYS[env_key] unless env_key.is_a?(String) && env_key.start_with?('HTTP_') + return nil if env_key == PROTOCOL_ENV_KEY + + env_key.delete_prefix('HTTP_').downcase.tr('_', '-') end # @param headers [Hash{String => String}] lowercase header names diff --git a/spec/posthog/mcp/request_scope_spec.rb b/spec/posthog/mcp/request_scope_spec.rb new file mode 100644 index 00000000..ab15f128 --- /dev/null +++ b/spec/posthog/mcp/request_scope_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' + +RSpec.describe PostHog::MCP::RequestScope do + describe '.headers_from_env' do + { + 'a prefixed header' => [{ 'HTTP_AUTHORIZATION' => 'Bearer t' }, { 'authorization' => 'Bearer t' }], + 'a multi-word header' => [{ 'HTTP_X_TENANT_ID' => 'acme' }, { 'x-tenant-id' => 'acme' }], + 'the headers Rack stores unprefixed' => [{ 'CONTENT_TYPE' => 'application/json', 'CONTENT_LENGTH' => '42' }, + { 'content-type' => 'application/json', 'content-length' => '42' }], + 'the protocol some servers store as HTTP_VERSION' => [{ 'HTTP_VERSION' => 'HTTP/1.1' }, {}], + 'other CGI variables' => [{ 'REQUEST_METHOD' => 'POST', 'PATH_INFO' => '/', 'rack.input' => 'x' }, {}], + 'an empty value' => [{ 'HTTP_X_API_KEY' => '' }, {}], + 'a non-String value' => [{ 'HTTP_X_COUNT' => 1 }, {}], + 'a non-Hash env' => [nil, {}] + }.each do |label, (env, expected)| + it "maps #{label}" do + expect(described_class.headers_from_env(env)).to eq(expected) + end + end + + it 'returns a frozen Hash, so a callback cannot change the session headers instrumentation reads later' do + expect(described_class.headers_from_env('HTTP_MCP_SESSION_ID' => 'abc')).to be_frozen + end + end +end From 226d454b6264a4e6f5d2a5c401f269618efd462b Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Tue, 27 Oct 2026 21:59:20 -0300 Subject: [PATCH 3/6] fix(mcp): give callbacks a copy of the headers instead of freezing them PostHog Review P2: a callback that mutates a frozen Hash raises FrozenError, whose message inspects the whole Hash (authorization, cookies), and the callback rescue paths log e.message through Log.debug. Instrumentation#extra now hands callbacks @headers.dup, so a mutation stays in the callback's copy and session/protocol resolution still reads the request's own headers. Tested: new http_transport_spec example where identify deletes user-agent and forges mcp-session-id; $mcp_tool_call keeps both. Fails without .dup. Full bundle exec rspec: 1905 examples, 0 failures. rubocop clean. --- .changeset/mcp-request-headers.md | 2 +- lib/posthog/mcp/instrumentation.rb | 3 ++- lib/posthog/mcp/request_scope.rb | 7 +++---- spec/posthog/mcp/http_transport_spec.rb | 15 +++++++++++++++ spec/posthog/mcp/request_scope_spec.rb | 4 ---- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.changeset/mcp-request-headers.md b/.changeset/mcp-request-headers.md index b3907ad0..7507ef12 100644 --- a/.changeset/mcp-request-headers.md +++ b/.changeset/mcp-request-headers.md @@ -2,4 +2,4 @@ "posthog-ruby": minor --- -MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names, frozen), so a callback can resolve a user from an `Authorization` bearer token or an API key. The SDK never captures header values. Resolve a header to an id instead of returning its raw value in identity properties or `event_properties`, which are sent unredacted. +MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names, a copy per callback), so a callback can resolve a user from an `Authorization` bearer token or an API key. The SDK never captures header values. Resolve a header to an id instead of returning its raw value in identity properties or `event_properties`, which are sent unredacted. diff --git a/lib/posthog/mcp/instrumentation.rb b/lib/posthog/mcp/instrumentation.rb index 09ed4e3a..5ba5e488 100644 --- a/lib/posthog/mcp/instrumentation.rb +++ b/lib/posthog/mcp/instrumentation.rb @@ -588,7 +588,8 @@ def extra 'session_id' => (@session.respond_to?(:session_id) ? @session.session_id : nil) || header_session_id, 'request_id' => @request_id, 'protocol_version' => protocol_version, - 'headers' => @headers, + # A copy, because session and protocol resolution read @headers after callbacks run. + 'headers' => @headers.dup, 'session' => @session } end diff --git a/lib/posthog/mcp/request_scope.rb b/lib/posthog/mcp/request_scope.rb index 2f35c704..0bb90e16 100644 --- a/lib/posthog/mcp/request_scope.rb +++ b/lib/posthog/mcp/request_scope.rb @@ -51,15 +51,14 @@ def current=(value) # identity properties or `event_properties` sends it, unredacted. # # @param env [Hash] a Rack `env` - # @return [Hash{String => String}] lowercase header names, frozen because - # {Instrumentation} reads session and protocol headers from it after callbacks run + # @return [Hash{String => String}] lowercase header names def headers_from_env(env) - return {}.freeze unless env.is_a?(Hash) + return {} unless env.is_a?(Hash) env.each_with_object({}) do |(key, value), acc| name = header_name(key) acc[name] = value if name && value.is_a?(String) && !value.empty? - end.freeze + end end def header_name(env_key) diff --git a/spec/posthog/mcp/http_transport_spec.rb b/spec/posthog/mcp/http_transport_spec.rb index 6eeb59f8..bfa99510 100644 --- a/spec/posthog/mcp/http_transport_spec.rb +++ b/spec/posthog/mcp/http_transport_spec.rb @@ -112,6 +112,21 @@ def parse(response) end end + it 'keeps the headers instrumentation reads when a callback changes its copy' do + identify = lambda do |_request, extra| + extra['headers'].delete('user-agent') + extra['headers']['mcp-session-id'] = 'forged' + { distinct_id: 'user-1' } + end + PostHog::MCP.instrument(server, client, identify: identify, enable_conversation_id: false) + token = transport.call(env_for(initialize_body, 'user-agent' => 'claude-code/2.1.0'))[1]['mcp-session-id'] + transport.call(env_for(rpc(2, 'tools/call', { name: 'ping_tool', arguments: {} }), 'mcp-session-id' => token, 'user-agent' => 'claude-code/2.1.0')) + + call = events_named(drain_events(client), '$mcp_tool_call').first + expect(call[:properties]).to include('$session_id' => PostHog::MCP.decode_session_id(token).session_id, + '$mcp_client_user_agent' => 'claude-code/2.1.0') + end + it 'leaves uninstrumented servers alone' do response = transport.call(env_for(initialize_body)) expect(response[0]).to eq(200) diff --git a/spec/posthog/mcp/request_scope_spec.rb b/spec/posthog/mcp/request_scope_spec.rb index ab15f128..4f5cdb9a 100644 --- a/spec/posthog/mcp/request_scope_spec.rb +++ b/spec/posthog/mcp/request_scope_spec.rb @@ -19,9 +19,5 @@ expect(described_class.headers_from_env(env)).to eq(expected) end end - - it 'returns a frozen Hash, so a callback cannot change the session headers instrumentation reads later' do - expect(described_class.headers_from_env('HTTP_MCP_SESSION_ID' => 'abc')).to be_frozen - end end end From f7ee18d399542d7697b237f8248b29e16ad36eeb Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 28 Oct 2026 10:55:55 -0300 Subject: [PATCH 4/6] docs(mcp): say callbacks share one headers copy per request --- .changeset/mcp-request-headers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mcp-request-headers.md b/.changeset/mcp-request-headers.md index 7507ef12..49c11cb5 100644 --- a/.changeset/mcp-request-headers.md +++ b/.changeset/mcp-request-headers.md @@ -2,4 +2,4 @@ "posthog-ruby": minor --- -MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names, a copy per callback), so a callback can resolve a user from an `Authorization` bearer token or an API key. The SDK never captures header values. Resolve a header to an id instead of returning its raw value in identity properties or `event_properties`, which are sent unredacted. +MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names, in a copy, so a callback that changes it cannot change how the SDK resolves the session), so a callback can resolve a user from an `Authorization` bearer token or an API key. The SDK never captures header values. Resolve a header to an id instead of returning its raw value in identity properties or `event_properties`, which are sent unredacted. From 78081845cd4e63151db9cbe666cfc08b7595abd3 Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Wed, 28 Oct 2026 11:58:16 -0300 Subject: [PATCH 5/6] fix(mcp): deep-copy header values handed to callbacks @headers.dup shared the Rack env's String values, so an in-place edit in a callback (delete_prefix!, replace) changed env['HTTP_AUTHORIZATION'] and the mcp-session-id the SDK resolves the session from. Copy each value too. Tested: the parameterized http_transport spec now edits values in place and asserts the env and $session_id are unchanged; it failed before the fix (env became forged/''/secret). bundle exec rspec: 1906 examples, 0 failures, 40 pending. rubocop clean. --- lib/posthog/mcp/instrumentation.rb | 5 ++-- spec/posthog/mcp/http_transport_spec.rb | 40 +++++++++++++++++-------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/posthog/mcp/instrumentation.rb b/lib/posthog/mcp/instrumentation.rb index 5ba5e488..c339e2e8 100644 --- a/lib/posthog/mcp/instrumentation.rb +++ b/lib/posthog/mcp/instrumentation.rb @@ -588,8 +588,9 @@ def extra 'session_id' => (@session.respond_to?(:session_id) ? @session.session_id : nil) || header_session_id, 'request_id' => @request_id, 'protocol_version' => protocol_version, - # A copy, because session and protocol resolution read @headers after callbacks run. - 'headers' => @headers.dup, + # A deep copy, because session and protocol resolution read @headers after + # callbacks run, and the values are the Rack env's own Strings. + 'headers' => @headers.transform_values(&:dup), 'session' => @session } end diff --git a/spec/posthog/mcp/http_transport_spec.rb b/spec/posthog/mcp/http_transport_spec.rb index bfa99510..2aa6d719 100644 --- a/spec/posthog/mcp/http_transport_spec.rb +++ b/spec/posthog/mcp/http_transport_spec.rb @@ -112,19 +112,35 @@ def parse(response) end end - it 'keeps the headers instrumentation reads when a callback changes its copy' do - identify = lambda do |_request, extra| - extra['headers'].delete('user-agent') - extra['headers']['mcp-session-id'] = 'forged' - { distinct_id: 'user-1' } + { + 'replaces entries' => lambda { |headers| + headers.delete('user-agent') + headers['authorization'] = 'forged' + headers['mcp-session-id'] = 'forged' + }, + 'edits values in place' => lambda { |headers| + headers['user-agent'].clear + headers['authorization'].delete_prefix!('Bearer ') + headers['mcp-session-id'].replace('forged') + } + }.each do |change, mutate| + it "keeps the request's headers when a callback #{change} in its copy" do + identify = lambda do |_request, extra| + mutate.call(extra['headers']) + { distinct_id: 'user-1' } + end + PostHog::MCP.instrument(server, client, identify: identify, enable_conversation_id: false) + token = transport.call(env_for(initialize_body, 'user-agent' => +'claude-code/2.1.0'))[1]['mcp-session-id'] + env = env_for(rpc(2, 'tools/call', { name: 'ping_tool', arguments: {} }), + 'mcp-session-id' => token.dup, 'user-agent' => +'claude-code/2.1.0', 'authorization' => +'Bearer secret') + transport.call(env) + + expect(env.values_at('HTTP_MCP_SESSION_ID', 'HTTP_USER_AGENT', 'HTTP_AUTHORIZATION')) + .to eq([token, 'claude-code/2.1.0', 'Bearer secret']) + call = events_named(drain_events(client), '$mcp_tool_call').first + expect(call[:properties]).to include('$session_id' => PostHog::MCP.decode_session_id(token).session_id, + '$mcp_client_user_agent' => 'claude-code/2.1.0') end - PostHog::MCP.instrument(server, client, identify: identify, enable_conversation_id: false) - token = transport.call(env_for(initialize_body, 'user-agent' => 'claude-code/2.1.0'))[1]['mcp-session-id'] - transport.call(env_for(rpc(2, 'tools/call', { name: 'ping_tool', arguments: {} }), 'mcp-session-id' => token, 'user-agent' => 'claude-code/2.1.0')) - - call = events_named(drain_events(client), '$mcp_tool_call').first - expect(call[:properties]).to include('$session_id' => PostHog::MCP.decode_session_id(token).session_id, - '$mcp_client_user_agent' => 'claude-code/2.1.0') end it 'leaves uninstrumented servers alone' do From 2bce651d7da4eb371743d1df64047013226e14af Mon Sep 17 00:00:00 2001 From: Lucas Faria Date: Thu, 29 Oct 2026 14:42:40 -0300 Subject: [PATCH 6/6] docs(mcp): say the header bag isn't captured, not that no header is User-Agent and X-Anthropic-Client are still captured as $mcp_client_user_agent and $mcp_vendor_client, so 'the SDK never captures header values' was false. Comment- and changeset-only; no runtime change. --- .changeset/mcp-request-headers.md | 2 +- lib/posthog/mcp/request_scope.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/mcp-request-headers.md b/.changeset/mcp-request-headers.md index 49c11cb5..e3acf427 100644 --- a/.changeset/mcp-request-headers.md +++ b/.changeset/mcp-request-headers.md @@ -2,4 +2,4 @@ "posthog-ruby": minor --- -MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names, in a copy, so a callback that changes it cannot change how the SDK resolves the session), so a callback can resolve a user from an `Authorization` bearer token or an API key. The SDK never captures header values. Resolve a header to an id instead of returning its raw value in identity properties or `event_properties`, which are sent unredacted. +MCP: `identify`, `intent_fallback` and `event_properties` callbacks now receive every HTTP request header in `extra['headers']` (lowercase names, in a copy, so a callback that changes it cannot change how the SDK resolves the session), so a callback can resolve a user from an `Authorization` bearer token or an API key. The SDK doesn't capture this header bag automatically, and the existing `User-Agent` and `X-Anthropic-Client` telemetry (`$mcp_client_user_agent`, `$mcp_vendor_client`) is unchanged. Resolve a header to an id instead of returning its raw value in identity properties or `event_properties`, which are sent unredacted. diff --git a/lib/posthog/mcp/request_scope.rb b/lib/posthog/mcp/request_scope.rb index 0bb90e16..aedf914f 100644 --- a/lib/posthog/mcp/request_scope.rb +++ b/lib/posthog/mcp/request_scope.rb @@ -45,9 +45,9 @@ def current=(value) # Every request header, so `identify`, `intent_fallback` and `event_properties` # can read the ones they need (an `authorization` bearer token, an API key) - # from `extra['headers']`. The SDK captures no header value: {Identity} keeps - # only scalars from `extra`, and {TransportIdentity} stamps `user-agent` and - # `x-anthropic-client` alone. A callback that returns a raw header value in + # from `extra['headers']`. The SDK doesn't capture this bag: {Identity} keeps + # only scalars from `extra`. {TransportIdentity} still stamps `user-agent` and + # `x-anthropic-client`, as before. A callback that returns a raw header value in # identity properties or `event_properties` sends it, unredacted. # # @param env [Hash] a Rack `env`