Module: PostHog::MCP

Defined in:
lib/posthog/mcp.rb,
lib/posthog/mcp/ids.rb,
lib/posthog/mcp/log.rb,
lib/posthog/mcp/sink.rb,
lib/posthog/mcp/tools.rb,
lib/posthog/mcp/client.rb,
lib/posthog/mcp/intent.rb,
lib/posthog/mcp/options.rb,
lib/posthog/mcp/session.rb,
lib/posthog/mcp/identity.rb,
lib/posthog/mcp/analytics.rb,
lib/posthog/mcp/constants.rb,
lib/posthog/mcp/exceptions.rb,
lib/posthog/mcp/truncation.rb,
lib/posthog/mcp/sanitization.rb,
lib/posthog/mcp/event_builder.rb,
lib/posthog/mcp/request_scope.rb,
lib/posthog/mcp/session_token.rb,
lib/posthog/mcp/tracking_data.rb,
lib/posthog/mcp/conversation_id.rb,
lib/posthog/mcp/instrumentation.rb,
lib/posthog/mcp/rack_middleware.rb,
lib/posthog/mcp/schema_mutation.rb,
lib/posthog/mcp/server_extension.rb

Overview

Note:

Experimental and not officially supported: no support is provided for this integration, and its API and the captured event schema may change in a minor release. A warning is logged when this file is required. Docs: https://posthog.com/docs/mcp-analytics

PostHog MCP analytics for servers built on the official Ruby mcp gem.

Wrap an MCP::Server so every tool call, handshake, listing, prompt, resource read, and failure is captured to PostHog as a $mcp_* event.

Examples:

require 'posthog/mcp'

posthog = PostHog::Client.new(api_key: 'phc_...', host: 'https://us.i.posthog.com')
server = MCP::Server.new(name: 'my-server', version: '1.0.0', tools: [MyTool])
analytics = PostHog::MCP.instrument(server, posthog)

# With posthog-rails the client is resolved from PostHog.client:
PostHog::MCP.instrument(server)

Defined Under Namespace

Modules: Callbacks, ConversationId, Event, EventBuilder, EventType, Exceptions, Identity, Ids, Intent, Log, ModelCapture, Property, RequestScope, Sanitization, SchemaMutation, ServerExtension, Session, SessionToken, Tools, TransportExtension, TransportIdentity, Truncation Classes: Analytics, Client, ContextOptions, IdentityCache, Instrumentation, ModelOptions, NoopAnalytics, Options, PreparedToolCall, RackMiddleware, SessionTokenPayload, Sink, TrackingData, UserIdentity

Constant Summary collapse

EXPERIMENTAL_NOTICE =
'PostHog::MCP is experimental and not officially supported: no support is provided for it, and its ' \
'API and the captured $mcp_* event schema may change in a minor release. Docs: ' \
'https://posthog.com/docs/mcp-analytics. Feedback welcome at https://github.com/PostHog/posthog-ruby/issues.'
SOURCE =

Value of $mcp_source on every primary $mcp_* event.

'posthog_mcp_analytics'
LIB_NAME =

$lib stamped on MCP analytics events (per event, never on the client).

'posthog-ruby-mcp'
INACTIVITY_TIMEOUT_MINUTES =

Generated (in-memory) sessions roll over after this much inactivity.

30
MCP_SESSION_HEADER =

Header carrying the transport session id, and our self-encoded token.

'mcp-session-id'
DEFAULT_CONTEXT_PARAMETER_DESCRIPTION =

Description of the injected context argument.

'Explain in 15-25 words, in third person, why this tool is called and how it supports ' \
"the user's goal. For analytics only. You MUST describe only the abstract purpose of the " \
'tool call. NEVER include, repeat, paraphrase, or infer personal, sensitive, or identifying ' \
'information from the user request or tool results, including names, emails, phone numbers, ' \
'IPs, IDs, or credentials. You MUST generalize specific entities into roles such as "a user", ' \
'"the customer", or "an account". Example: "Retrieving a customer\'s recent orders to ' \
'investigate a billing issue and help support determine the appropriate resolution."'
DEFAULT_MODEL_PARAMETER_DESCRIPTION =

Description of the injected llm_model argument.

'The exact model identifier you (the assistant) are running as, taken from your system ' \
'prompt or environment (e.g. "claude-opus-4-8", "gpt-5.2"). Used for analytics only. If you ' \
'do not know your model identifier with certainty, pass "unknown" — never guess.'
DEFAULT_CONVERSATION_ID_DESCRIPTION =

Description of the injected conversation_id argument.

"Echo the conversation_id from the server's previous response. The server provides it on " \
'the first call — never invent one, and do not issue parallel tool calls until you have it.'

Class Method Summary collapse

Class Method Details

.decode_session_id(value) ⇒ PostHog::MCP::SessionTokenPayload?

Decode an Mcp-Session-Id value; nil for anything that is not one of our tokens.



116
117
118
# File 'lib/posthog/mcp.rb', line 116

def decode_session_id(value)
  SessionToken.decode(value)
end

.derive_session_id_from_conversation(conversation_id) ⇒ String

Deterministic $session_id for an agent conversation handle.

Returns:

  • (String)


130
131
132
# File 'lib/posthog/mcp.rb', line 130

def derive_session_id_from_conversation(conversation_id)
  Session.derive_session_id_from_conversation(conversation_id)
end

.derive_session_id_from_mcp_session(mcp_session_id) ⇒ String

Deterministic $session_id for a transport session id (stable across restarts).

Returns:

  • (String)


123
124
125
# File 'lib/posthog/mcp.rb', line 123

def derive_session_id_from_mcp_session(mcp_session_id)
  Session.derive_session_id_from_mcp_session(mcp_session_id)
end

.encode_session_id(payload) ⇒ String

Encode a session token for a custom HTTP layer's Mcp-Session-Id response header.

Parameters:

Returns:

  • (String)


109
110
111
# File 'lib/posthog/mcp.rb', line 109

def encode_session_id(payload)
  SessionToken.encode(payload)
end

.experimental_notice!(options = nil) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



147
148
149
150
151
152
153
# File 'lib/posthog/mcp.rb', line 147

def experimental_notice!(options = nil)
  Log.debug(options, EXPERIMENTAL_NOTICE)
  return if @experimental_notice_shown

  @experimental_notice_shown = true
  Kernel.warn("[posthog-ruby] #{EXPERIMENTAL_NOTICE}")
end

.get_more_tools_result ⇒ Hash

The canned get_more_tools result for custom dispatchers.

Returns:

  • (Hash)


137
138
139
# File 'lib/posthog/mcp.rb', line 137

def get_more_tools_result # rubocop:disable Naming/AccessorMethodName -- public API name
  Tools.result
end

.instrument(server, client = nil, options: nil, **kwargs) ⇒ PostHog::MCP::Analytics

Instrument an MCP::Server.

Parameters:

  • server (MCP::Server) —

    the server to wrap

  • client (PostHog::Client, nil) (defaults to: nil) —

    the PostHog client to send through. Defaults to PostHog.client when the posthog-rails facade is loaded.

  • options (PostHog::MCP::Options, nil) (defaults to: nil) —

    prebuilt options; otherwise pass keywords

  • kwargs (Hash) —

    Options keywords (identify:, before_send:, ...)

Returns:

  • (PostHog::MCP::Analytics) —

    handle for custom events; a no-op handle when instrumentation fails (logged, never raised)

Raises:

  • (LoadError) —

    when the mcp gem is not available



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/posthog/mcp.rb', line 66

def instrument(server, client = nil, options: nil, **kwargs)
  opts = options.is_a?(Options) ? options : Options.new(**kwargs)
  ensure_mcp_sdk!
  experimental_notice!(opts)

  begin
    unless server.is_a?(::MCP::Server)
      raise TypeError, "Unsupported server type: #{server.class}. Pass an MCP::Server."
    end

    existing = tracking_data(server)
    if existing
      Log.debug(opts, 'instrument() - server already instrumented, skipping initialization')
      return Analytics.new(server)
    end

    resolved_client = resolve_client(client)
    Log.warn(opts, 'Warning: no PostHog client available; MCP events will not be sent.') if resolved_client.nil?
    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!
    server.instance_variable_set(:@__posthog_mcp, data)
    register_missing_capability_tool(server, data)
    Analytics.new(server)
  rescue StandardError => e
    Log.warn(opts, "Warning: failed to instrument server - #{e.class}: #{e.message}")
    NoopAnalytics.new
  end
end

.mcp_sdk_available? ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


142
143
144
# File 'lib/posthog/mcp.rb', line 142

def mcp_sdk_available?
  defined?(::MCP::Server) ? true : false
end

.reset_for_tests! ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



156
157
158
# File 'lib/posthog/mcp.rb', line 156

def reset_for_tests!
  @experimental_notice_shown = false
end

.tracking_data(server) ⇒ PostHog::MCP::TrackingData?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:



99
100
101
102
103
# File 'lib/posthog/mcp.rb', line 99

def tracking_data(server)
  return nil unless server.instance_variable_defined?(:@__posthog_mcp)

  server.instance_variable_get(:@__posthog_mcp)
end