Module: Legion::MCP::FunctionDiscovery

Extended by:
Logging::Helper
Defined in:
lib/legion/mcp/function_discovery.rb

Class Method Summary collapse

Class Method Details

.build_tool_class(opts) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/legion/mcp/function_discovery.rb', line 133

def build_tool_class(opts)
  runner_ref         = opts[:runner_module]
  func_ref           = opts[:function_name]
  tool_name_value    = opts[:name]
  description_value  = opts[:description]
  input_schema_value = opts[:input_schema]
  mcp_category_value = opts[:mcp_category]
  mcp_tier_value     = opts[:mcp_tier]
  klass = Class.new(::MCP::Tool) do
    tool_name tool_name_value
    description description_value
    input_schema(input_schema_value)
    define_singleton_method(:mcp_category) { mcp_category_value }
    define_singleton_method(:mcp_tier)     { mcp_tier_value }
  end
  wire_call_method(klass, runner_ref, func_ref)
  klass
end

.build_tool_opts(runner_module, func_name, meta, opts, defn) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/legion/mcp/function_discovery.rb', line 75

def build_tool_opts(runner_module, func_name, meta, opts, defn)
  prefix = defn&.dig(:mcp_prefix) || opts[:prefix]
  {
    name:          derive_tool_name(func_name, prefix),
    description:   meta[:desc] || defn&.dig(:desc) || "Auto-discovered: #{func_name}",
    input_schema:  meta[:options] || { properties: {} },
    runner_module: runner_module,
    function_name: func_name,
    mcp_category:  defn&.dig(:mcp_category),
    mcp_tier:      defn&.dig(:mcp_tier)
  }
end

.build_tools_from_runner(runner_module) ⇒ Object



42
43
44
45
46
47
48
49
50
# File 'lib/legion/mcp/function_discovery.rb', line 42

def build_tools_from_runner(runner_module)
  return unless runner_module.respond_to?(:settings) && runner_module.settings.is_a?(Hash)

  functions = runner_module.settings[:functions]
  return if functions.nil? || functions.empty?

  opts = runner_expose_opts(runner_module)
  functions.each { |func_name, meta| register_function(runner_module, func_name, meta, opts) }
end

.definition_for(runner_module, func_name) ⇒ Object

Returns the definition hash for a method on a runner module, or nil if not available.



89
90
91
92
93
# File 'lib/legion/mcp/function_discovery.rb', line 89

def definition_for(runner_module, func_name)
  return nil unless runner_module.respond_to?(:definition_for)

  runner_module.definition_for(func_name)
end

.deps_satisfied?(deps) ⇒ Boolean

Returns:

  • (Boolean)


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/legion/mcp/function_discovery.rb', line 116

def deps_satisfied?(deps)
  return true if deps.nil? || deps.empty?

  deps.all? do |dep|
    parts = dep.delete_prefix('::').split('::').reject(&:empty?)
    current = Object
    parts.all? do |part|
      if current.const_defined?(part, false)
        current = current.const_get(part, false)
        true
      else
        false
      end
    end
  end
end

.derive_tool_name(func_name, prefix) ⇒ Object



111
112
113
114
# File 'lib/legion/mcp/function_discovery.rb', line 111

def derive_tool_name(func_name, prefix)
  base = prefix || 'legion.generated'
  "#{base}.#{func_name}"
end

.discover_and_registerObject

rubocop:disable Metrics/PerceivedComplexity



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/legion/mcp/function_discovery.rb', line 10

def discover_and_register # rubocop:disable Metrics/PerceivedComplexity
  return if @discovery_fired

  @discovery_fired = true

  if defined?(Legion::Tools::Discovery) && Legion::Tools::Discovery.respond_to?(:discover_and_register)
    Legion::Tools::Discovery.discover_and_register
    return
  end

  return unless defined?(Legion::Extensions)

  extensions =
    if Legion::Extensions.respond_to?(:extensions)
      Legion::Extensions.extensions || []
    else
      Legion::Extensions.instance_variable_get(:@extensions) || []
    end
  extensions.each do |ext|
    next unless ext.respond_to?(:runner_modules)

    ext.runner_modules.each { |runner_mod| build_tools_from_runner(runner_mod) }
  rescue StandardError => e
    handle_exception(e, level: :debug, operation: 'legion.mcp.function_discovery.discover_and_register')
    log.debug("FunctionDiscovery: skipping #{ext}: #{e.message}")
  end
end

.register_function(runner_module, func_name, meta, opts) ⇒ Object



57
58
59
60
61
62
63
64
65
# File 'lib/legion/mcp/function_discovery.rb', line 57

def register_function(runner_module, func_name, meta, opts)
  defn = definition_for(runner_module, func_name)
  return unless resolve_exposed(defn, meta, opts)

  requires = defn&.dig(:requires)&.map(&:to_s) || meta[:requires]
  return unless deps_satisfied?(requires)

  Server.register_tool(build_tool_class(build_tool_opts(runner_module, func_name, meta, opts, defn)))
end

.reset_discovery!Object



38
39
40
# File 'lib/legion/mcp/function_discovery.rb', line 38

def reset_discovery!
  @discovery_fired = false
end

.resolve_exposed(defn, meta, opts) ⇒ Object



67
68
69
70
71
72
73
# File 'lib/legion/mcp/function_discovery.rb', line 67

def resolve_exposed(defn, meta, opts)
  if defn.nil?
    should_expose?(meta, opts[:class_expose], opts[:global_expose])
  else
    should_expose_from_definition?(defn, meta, opts[:class_expose], opts[:global_expose])
  end
end

.runner_expose_opts(_runner_module) ⇒ Object



52
53
54
55
# File 'lib/legion/mcp/function_discovery.rb', line 52

def runner_expose_opts(_runner_module)
  global_expose = defined?(Legion::Settings) ? (Legion::Settings.dig(:mcp, :auto_expose_runners) || false) : false
  { class_expose: nil, global_expose: global_expose, prefix: nil }
end

.should_expose?(func_meta, class_level, global_default) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
107
108
109
# File 'lib/legion/mcp/function_discovery.rb', line 104

def should_expose?(func_meta, class_level, global_default)
  return func_meta[:expose] unless func_meta[:expose].nil?
  return class_level unless class_level.nil?

  global_default || false
end

.should_expose_from_definition?(defn, func_meta, class_level, global_default) ⇒ Boolean

Exposure check when a definition is present. definition takes highest precedence; falls back to legacy path.

Returns:

  • (Boolean)


97
98
99
100
101
102
# File 'lib/legion/mcp/function_discovery.rb', line 97

def should_expose_from_definition?(defn, func_meta, class_level, global_default)
  mcp_exposed = defn[:mcp_exposed]
  return mcp_exposed unless mcp_exposed.nil?

  should_expose?(func_meta, class_level, global_default)
end

.wire_call_method(klass, runner_ref, func_ref) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/legion/mcp/function_discovery.rb', line 152

def wire_call_method(klass, runner_ref, func_ref)
  klass.define_singleton_method(:call) do |**params|
    error = false
    result =
      if runner_ref.respond_to?(func_ref)
        begin
          runner_ref.public_send(func_ref, **params)
        rescue StandardError => e
          handle_exception(e, level: :warn, operation: 'legion.mcp.function_discovery.call')
          error = true
          { error: e.message }
        end
      else
        error = true
        { error: "function #{func_ref} not found" }
      end
    text = defined?(Legion::JSON) ? Legion::JSON.dump(result) : result.to_s
    ::MCP::Tool::Response.new([{ type: 'text', text: text }], error: error)
  end
end