Class: PostHog::MCP::Client

Inherits:
Client
  • Object
show all
Defined in:
lib/posthog/mcp/client.rb

Overview

Note:

Experimental.

A Client with first-class MCP analytics for custom dispatchers (your own HTTP layer, no MCP::Server to wrap). The host resolves identity and context per request and calls the capture methods directly; events flow through the same sanitize -> truncate -> $exception fan-out pipeline as instrument. Does not need the mcp gem.

Examples:

posthog = PostHog::MCP::Client.new(api_key: 'phc_...', host: 'https://us.i.posthog.com')
posthog.capture_tool_call('search_docs', duration_ms: 42, distinct_id: 'user_123')

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Client

Returns a new instance of Client.

Parameters:

  • opts (Hash) (defaults to: {}) —

    Client options plus:

Options Hash (opts):

  • :missing_capability_tool_name (String) —

    name of the virtual tool (default get_more_tools)

  • :mcp_exception_autocapture (Boolean) —

    emit a sibling $exception for failed calls (default true)

  • :capture_model (Boolean, ModelOptions) —

    capture the calling model (default true)



21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/posthog/mcp/client.rb', line 21

def initialize(opts = {})
  opts = opts.transform_keys(&:to_sym)
  @missing_capability_tool_name = opts.delete(:missing_capability_tool_name) || Tools::GET_MORE_TOOLS_NAME
  @mcp_exception_autocapture = opts.delete(:mcp_exception_autocapture) != false
  capture_model = opts.key?(:capture_model) ? opts.delete(:capture_model) : true
  @capture_model_option = Options.new(capture_model: capture_model).capture_model
  super
  @mcp_sink = Sink.new(self)
  @mcp_options = Options.new(
    enable_exception_autocapture: @mcp_exception_autocapture,
    missing_capability_tool_name: @missing_capability_tool_name
  )
end

Instance Method Details

#capture_initialize(client_name: nil, client_version: nil, protocol_version: nil, parameters: nil, response: nil, duration_ms: nil, distinct_id: nil, session_id: nil, client_user_agent: nil, vendor_client: nil, set_properties: nil, groups: nil, properties: nil, timestamp: nil) ⇒ void

This method returns an undefined value.

Capture the connection handshake. Emits $mcp_initialize.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/posthog/mcp/client.rb', line 66

def capture_initialize(client_name: nil, client_version: nil, protocol_version: nil, parameters: nil,
                       response: nil, duration_ms: nil, distinct_id: nil, session_id: nil,
                       client_user_agent: nil, vendor_client: nil, set_properties: nil, groups: nil,
                       properties: nil, timestamp: nil)
  event = base_event(EventType::MCP_INITIALIZE, distinct_id, session_id, set_properties, groups, properties,
                     timestamp, client_user_agent, vendor_client)
  event['client_name'] = client_name
  event['client_version'] = client_version
  event['protocol_version'] = protocol_version
  event['parameters'] = parameters
  event['response'] = response
  event['duration'] = duration_ms
  emit(event)
end

#capture_missing_capability(context: nil, parameters: nil, protocol_version: nil, distinct_id: nil, session_id: nil, client_user_agent: nil, vendor_client: nil, set_properties: nil, groups: nil, properties: nil, timestamp: nil, llm_model: nil, llm_model_source: nil) ⇒ void

This method returns an undefined value.

Capture a get_more_tools call as a missing-capability report. Emits $mcp_missing_capability with the agent's description as $mcp_intent.



105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/posthog/mcp/client.rb', line 105

def capture_missing_capability(context: nil, parameters: nil, protocol_version: nil, distinct_id: nil,
                               session_id: nil, client_user_agent: nil, vendor_client: nil,
                               set_properties: nil, groups: nil, properties: nil, timestamp: nil,
                               llm_model: nil, llm_model_source: nil)
  event = base_event(EventType::MCP_MISSING_CAPABILITY, distinct_id, session_id, set_properties, groups,
                     properties, timestamp, client_user_agent, vendor_client)
  event['resource_name'] = @missing_capability_tool_name
  event['protocol_version'] = protocol_version
  event['parameters'] = parameters
  apply_intent(event, context, 'context_parameter')
  apply_model(event, llm_model, llm_model_source)
  emit(event)
end

#capture_tool_call(tool_name, intent: nil, intent_source: nil, parameters: nil, response: nil, duration_ms: nil, is_error: false, error: nil, error_type: nil, category: nil, tool_description: nil, protocol_version: nil, distinct_id: nil, session_id: nil, client_user_agent: nil, vendor_client: nil, set_properties: nil, groups: nil, properties: nil, timestamp: nil, llm_model: nil, llm_model_source: nil) ⇒ void

This method returns an undefined value.

Capture a tool invocation. Emits $mcp_tool_call (+ $exception on error).



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/posthog/mcp/client.rb', line 38

def capture_tool_call(tool_name, intent: nil, intent_source: nil, parameters: nil, response: nil,
                      duration_ms: nil, is_error: false, error: nil, error_type: nil, category: nil,
                      tool_description: nil, protocol_version: nil, distinct_id: nil, session_id: nil,
                      client_user_agent: nil, vendor_client: nil, set_properties: nil, groups: nil,
                      properties: nil, timestamp: nil, llm_model: nil, llm_model_source: nil)
  event = base_event(EventType::MCP_TOOLS_CALL, distinct_id, session_id, set_properties, groups, properties,
                     timestamp, client_user_agent, vendor_client)
  event['resource_name'] = tool_name
  event['tool_description'] = tool_description
  event['tool_category'] = category
  event['protocol_version'] = protocol_version
  event['parameters'] = parameters
  event['response'] = response
  event['duration'] = duration_ms
  event['is_error'] = is_error == true
  event['error_type'] = error_type
  apply_intent(event, intent, intent_source)
  apply_model(event, llm_model, llm_model_source)
  if is_error
    event['error'] =
      Exceptions.capture_exception(error.nil? ? "Tool #{tool_name} returned an error" : error)
  end
  emit(event)
end

#capture_tools_list(tool_names: nil, parameters: nil, response: nil, duration_ms: nil, is_error: false, error: nil, error_type: nil, protocol_version: nil, distinct_id: nil, session_id: nil, client_user_agent: nil, vendor_client: nil, set_properties: nil, groups: nil, properties: nil, timestamp: nil) ⇒ void

This method returns an undefined value.

Capture a tools/list response. Emits $mcp_tools_list with $mcp_listed_tool_names.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/posthog/mcp/client.rb', line 84

def capture_tools_list(tool_names: nil, parameters: nil, response: nil, duration_ms: nil, is_error: false,
                       error: nil, error_type: nil, protocol_version: nil, distinct_id: nil, session_id: nil,
                       client_user_agent: nil, vendor_client: nil, set_properties: nil, groups: nil,
                       properties: nil, timestamp: nil)
  event = base_event(EventType::MCP_TOOLS_LIST, distinct_id, session_id, set_properties, groups, properties,
                     timestamp, client_user_agent, vendor_client)
  event['listed_tool_names'] = tool_names
  event['protocol_version'] = protocol_version
  event['parameters'] = parameters
  event['response'] = response
  event['duration'] = duration_ms
  event['is_error'] = is_error == true
  event['error_type'] = error_type
  event['error'] = Exceptions.capture_exception(error.nil? ? 'tools/list failed' : error) if is_error
  emit(event)
end

#prepare_tool_call(name, args = nil, input_schema: nil) ⇒ PreparedToolCall

Pull the agent's intent off the context argument and its self-reported model off llm_model, strip the arguments this integration injected, and flag the get_more_tools virtual tool. Hand intent/intent_source and llm_model/llm_model_source straight to #capture_tool_call.

Pass the tool's own inputSchema (the same Hash you handed to #prepare_tool_list) so a field the tool declares itself is left in args and never read as analytics: only an injected argument is stripped and reported. A composed (oneOf/allOf/anyOf) or $ref schema is never injected into, so its fields are the tool's own and are left alone too. Without the schema there is no way to tell the two apart, so the injected names are always stripped.

Parameters:

  • name (String) —

    tool name

  • args (Hash, nil) (defaults to: nil) —

    the call's arguments

  • input_schema (Hash, nil) (defaults to: nil) —

    the tool's raw inputSchema

Returns:



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/posthog/mcp/client.rb', line 173

def prepare_tool_call(name, args = nil, input_schema: nil)
  intent = tool_declares?(input_schema, 'context') ? nil : Intent.normalize(argument(args, 'context'))
  model = if @capture_model_option == false || tool_declares?(input_schema, ModelCapture::PARAM_NAME)
            nil
          else
            ModelCapture.normalize(argument(args, ModelCapture::PARAM_NAME))
          end
  PreparedToolCall.new(
    args: strip_injected(args, input_schema),
    intent: intent,
    intent_source: intent ? 'context_parameter' : nil,
    llm_model: model,
    llm_model_source: model ? 'self_reported' : nil,
    is_missing_capability: name == @missing_capability_tool_name
  )
end

#prepare_tool_list(tools, context: true, report_missing: false, capture_model: @capture_model_option) ⇒ Array<Hash>

Inject the context and llm_model arguments into every tool descriptor (Hash with inputSchema) so agents state their intent, and optionally append the get_more_tools virtual tool. Returns a new Array of new Hashes. A tool whose schema is composed (oneOf/allOf/anyOf) or a $ref is passed through untouched. Pass capture_model: false here or to #initialize to disable model capture for this client, including extraction from later tool calls.

Parameters:

  • tools (Array<Hash>) —

    tools/list entries

Returns:

  • (Array<Hash>)


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/posthog/mcp/client.rb', line 129

def prepare_tool_list(tools, context: true, report_missing: false, capture_model: @capture_model_option)
  options = Options.new(context: context, capture_model: capture_model)
  @capture_model_option = false unless options.capture_model_enabled?
  options = Options.new(context: context, capture_model: false) if @capture_model_option == false
  prepared = tools.map do |tool|
    next tool unless tool.is_a?(Hash) && (options.context_enabled? || options.capture_model_enabled?)

    name = SchemaMutation.fetch(tool, :name) || 'unknown'
    next tool if name == @missing_capability_tool_name

    schema = SchemaMutation.fetch(tool, :inputSchema)
    if options.context_enabled?
      schema = SchemaMutation.add_context_parameter(schema, tool_name: name,
                                                            description: options.context_description)
    end
    if options.capture_model_enabled?
      schema = SchemaMutation.add_model_parameter(schema, tool_name: name,
                                                          description: options.model_description)
    end
    tool.merge(SchemaMutation.key_for(tool, :inputSchema) => schema)
  end
  if report_missing && prepared.none? { |t| SchemaMutation.fetch(t, :name) == @missing_capability_tool_name }
    prepared << Tools.descriptor(@missing_capability_tool_name, options)
  end
  prepared
end