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)



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

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
  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.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/posthog/mcp/client.rb', line 63

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.



102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/posthog/mcp/client.rb', line 102

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).



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

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.



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

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:



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/posthog/mcp/client.rb', line 166

def prepare_tool_call(name, args = nil, input_schema: nil)
  intent = tool_declares?(input_schema, 'context') ? nil : Intent.normalize(argument(args, 'context'))
  model = if 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: false) ⇒ Array<Hash>

Inject the context argument (and, with capture_model, llm_model) 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.

Parameters:

  • tools (Array<Hash>) —

    tools/list entries

Returns:

  • (Array<Hash>)


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/posthog/mcp/client.rb', line 124

def prepare_tool_list(tools, context: true, report_missing: false, capture_model: false)
  options = Options.new(context: context, capture_model: capture_model)
  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