Module: PostHog::MCP::SchemaMutation Private

Defined in:
lib/posthog/mcp/schema_mutation.rb

Overview

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

Injects analytics parameters (context, conversation_id, llm_model) into advertised tool input schemas and declares _mcp_instructions on output schemas. Always returns new hashes: the mcp gem's Tool.to_h shares its nested schema hashes with the tool class, so in-place writes would leak into the tool permanently.

Works on symbol- or string-keyed schemas and writes back in the input's key style.

Constant Summary collapse

COMPLEX_KEYS =

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

%w[oneOf allOf anyOf].freeze

Class Method Summary collapse

Class Method Details

.add_context_parameter(schema, tool_name:, description: nil, required: true, 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.



113
114
115
116
# File 'lib/posthog/mcp/schema_mutation.rb', line 113

def add_context_parameter(schema, tool_name:, description: nil, required: true, options: nil)
  add_parameter(schema, 'context', description || DEFAULT_CONTEXT_PARAMETER_DESCRIPTION,
                tool_name: tool_name, required: required, options: options, label: 'context')
end

.add_conversation_id_parameter(schema, tool_name:, 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.



118
119
120
121
# File 'lib/posthog/mcp/schema_mutation.rb', line 118

def add_conversation_id_parameter(schema, tool_name:, options: nil)
  add_parameter(schema, ConversationId::PARAM_NAME, DEFAULT_CONVERSATION_ID_DESCRIPTION,
                tool_name: tool_name, required: false, options: options, label: 'conversation_id')
end

.add_model_parameter(schema, tool_name:, description: nil, required: true, 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.



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

def add_model_parameter(schema, tool_name:, description: nil, required: true, options: nil)
  add_parameter(schema, ModelCapture::PARAM_NAME, description || DEFAULT_MODEL_PARAMETER_DESCRIPTION,
                tool_name: tool_name, required: required, options: options, label: 'llm_model')
end

.add_output_instructions(schema, tool_name:, options: nil) ⇒ Array(Hash, 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.

Declare an optional _mcp_instructions on the output schema.

Returns:

  • (Array(Hash, Boolean)) —

    [schema, declared]



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/posthog/mcp/schema_mutation.rb', line 146

def add_output_instructions(schema, tool_name:, options: nil)
  return [schema, false] if schema.nil? || (schema.respond_to?(:empty?) && schema.empty?)

  key = ConversationId::MCP_INSTRUCTIONS_KEY
  unless declarable_output?(schema)
    properties = fetch(schema, :properties)
    if properties.is_a?(Hash) && declares_param?(schema, key)
      return [schema, true] if our_declaration?(fetch(properties, key))

      Log.debug(options,
                "WARN: Tool \"#{tool_name}\" already declares '#{key}' in its output schema. Leaving it alone.")
    else
      Log.debug(options, "WARN: Tool \"#{tool_name}\" has a complex output schema (oneOf/allOf/anyOf/$ref). " \
                         "Skipping '#{key}' declaration; its session handle stays content-only.")
    end
    return [schema, false]
  end

  schema = deep_dup(schema)
  properties_key = key_for(schema, :properties)
  schema[properties_key] = {} unless schema[properties_key].is_a?(Hash)
  property_key = string_keys?(schema[properties_key], schema) ? key : key.to_sym
  schema[properties_key][property_key] = {
    type: 'object',
    description: ConversationId::INSTRUCTIONS_FIELD_DESCRIPTION,
    properties: {
      conversation_id: { type: 'string', description: ConversationId::CONVERSATION_ID_FIELD_DESCRIPTION }
    }
  }
  [schema, true]
end

.add_parameter(schema, name, description, tool_name:, required:, options: nil, label: name) ⇒ Hash

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.

Add a string property to an object schema. Returns the input unchanged (logging a warning) when the property exists or the schema is composed or referenced.

Returns:

  • (Hash) —

    new schema



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/posthog/mcp/schema_mutation.rb', line 75

def add_parameter(schema, name, description, tool_name:, required:, options: nil, label: name)
  if declares_param?(schema, name)
    Log.debug(options,
              "WARN: Tool \"#{tool_name}\" already has '#{name}' parameter. Skipping #{label} injection.")
    return schema
  end
  unless injectable?(schema)
    Log.debug(options,
              "WARN: Tool \"#{tool_name}\" has a composed schema (oneOf/allOf/anyOf/$ref). " \
              "Skipping #{label} injection.")
    return schema
  end

  if schema.nil? || (schema.respond_to?(:empty?) && schema.empty?)
    schema = { type: 'object', properties: {},
               required: [] }
  end
  schema = deep_dup(schema)
  properties_key = key_for(schema, :properties)
  schema[properties_key] = {} unless schema[properties_key].is_a?(Hash)

  # `additionalProperties: false` stays: the injected name is listed under
  # `properties`, so it is still accepted, and relaxing the constraint would
  # advertise a looser schema than the dispatcher actually validates against.
  property_key = string_keys?(schema[properties_key], schema) ? name.to_s : name.to_sym
  schema[properties_key][property_key] = { type: 'string', description: description }

  if required
    required_key = key_for(schema, :required)
    if schema[required_key].is_a?(Array)
      schema[required_key] << name.to_s unless schema[required_key].map(&:to_s).include?(name.to_s)
    else
      schema[required_key] = [name.to_s]
    end
  end
  schema
end

.complex?(schema) ⇒ 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)


37
38
39
# File 'lib/posthog/mcp/schema_mutation.rb', line 37

def complex?(schema)
  COMPLEX_KEYS.any? { |key| truthy?(fetch(schema, key)) }
end

.declarable_output?(schema) ⇒ 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.

Whether _mcp_instructions can safely be declared on this output schema.

Returns:

  • (Boolean)


129
130
131
132
133
134
135
136
137
# File 'lib/posthog/mcp/schema_mutation.rb', line 129

def declarable_output?(schema)
  return false unless schema.is_a?(Hash)
  return false if truthy?(fetch(schema, :$ref)) || complex?(schema)

  properties = fetch(schema, :properties)
  return false if !properties.nil? && !properties.is_a?(Hash)

  !truthy?(properties) || !declares_param?(schema, ConversationId::MCP_INSTRUCTIONS_KEY)
end

.declares_param?(schema, name) ⇒ 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)


32
33
34
35
# File 'lib/posthog/mcp/schema_mutation.rb', line 32

def declares_param?(schema, name)
  properties = fetch(schema, :properties)
  properties.is_a?(Hash) && (properties.key?(name.to_sym) || properties.key?(name.to_s))
end

.deep_dup(value) ⇒ 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.



62
63
64
65
66
67
68
# File 'lib/posthog/mcp/schema_mutation.rb', line 62

def deep_dup(value)
  case value
  when Hash then value.to_h { |k, v| [k, deep_dup(v)] }
  when Array then value.map { |v| deep_dup(v) }
  else value
  end
end

.fetch(hash, name) ⇒ 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.



19
20
21
22
23
# File 'lib/posthog/mcp/schema_mutation.rb', line 19

def fetch(hash, name)
  return nil unless hash.is_a?(Hash)

  hash.key?(name.to_sym) ? hash[name.to_sym] : hash[name.to_s]
end

.injectable?(schema) ⇒ 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.

Whether an analytics parameter may be injected into (and therefore owned in) this input schema. A composed (oneOf/allOf/anyOf) or referenced ($ref) schema can declare the property out of band, and a sibling property next to a reference to a closed object makes the schema unsatisfiable, so those are left alone entirely.

Returns:

  • (Boolean)


46
47
48
49
50
# File 'lib/posthog/mcp/schema_mutation.rb', line 46

def injectable?(schema)
  return true unless schema.is_a?(Hash)

  !complex?(schema) && !truthy?(fetch(schema, :$ref))
end

.key_for(hash, name) ⇒ 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.



25
26
27
28
29
30
# File 'lib/posthog/mcp/schema_mutation.rb', line 25

def key_for(hash, name)
  return name.to_sym if hash.key?(name.to_sym)
  return name.to_s if hash.key?(name.to_s)

  hash.keys.first.is_a?(String) ? name.to_s : name.to_sym
end

.our_declaration?(declaration) ⇒ 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)


139
140
141
# File 'lib/posthog/mcp/schema_mutation.rb', line 139

def our_declaration?(declaration)
  declaration.is_a?(Hash) && fetch(declaration, :description) == ConversationId::INSTRUCTIONS_FIELD_DESCRIPTION
end

.string_keys?(hash, parent) ⇒ 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.

Key style of hash, falling back to parent's when hash is empty.

Returns:

  • (Boolean)


53
54
55
56
# File 'lib/posthog/mcp/schema_mutation.rb', line 53

def string_keys?(hash, parent)
  source = hash.empty? ? parent : hash
  source.keys.first.is_a?(String)
end

.truthy?(value) ⇒ 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)


58
59
60
# File 'lib/posthog/mcp/schema_mutation.rb', line 58

def truthy?(value)
  !(value.nil? || value == false || (value.respond_to?(:empty?) && value.empty?))
end