Module: Legion::MCP::FunctionDiscovery
- Extended by:
- Logging::Helper
- Defined in:
- lib/legion/mcp/function_discovery.rb
Class Method Summary collapse
- .build_tool_class(opts) ⇒ Object
- .build_tool_opts(runner_module, func_name, meta, opts, defn) ⇒ Object
- .build_tools_from_runner(runner_module) ⇒ Object
-
.definition_for(runner_module, func_name) ⇒ Object
Returns the definition hash for a method on a runner module, or nil if not available.
- .deps_satisfied?(deps) ⇒ Boolean
- .derive_tool_name(func_name, prefix) ⇒ Object
-
.discover_and_register ⇒ Object
rubocop:disable Metrics/PerceivedComplexity.
- .register_function(runner_module, func_name, meta, opts) ⇒ Object
- .reset_discovery! ⇒ Object
- .resolve_exposed(defn, meta, opts) ⇒ Object
- .runner_expose_opts(_runner_module) ⇒ Object
- .should_expose?(func_meta, class_level, global_default) ⇒ Boolean
-
.should_expose_from_definition?(defn, func_meta, class_level, global_default) ⇒ Boolean
Exposure check when a definition is present.
- .wire_call_method(klass, runner_ref, func_ref) ⇒ Object
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, , opts, defn) prefix = defn&.dig(:mcp_prefix) || opts[:prefix] { name: derive_tool_name(func_name, prefix), description: [:desc] || defn&.dig(:desc) || "Auto-discovered: #{func_name}", input_schema: [: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, | register_function(runner_module, func_name, , 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
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_register ⇒ Object
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.}") 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, , opts) defn = definition_for(runner_module, func_name) return unless resolve_exposed(defn, , opts) requires = defn&.dig(:requires)&.map(&:to_s) || [:requires] return unless deps_satisfied?(requires) Server.register_tool(build_tool_class(build_tool_opts(runner_module, func_name, , 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, , opts) if defn.nil? should_expose?(, opts[:class_expose], opts[:global_expose]) else should_expose_from_definition?(defn, , 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
104 105 106 107 108 109 |
# File 'lib/legion/mcp/function_discovery.rb', line 104 def should_expose?(, class_level, global_default) return [:expose] unless [: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.
97 98 99 100 101 102 |
# File 'lib/legion/mcp/function_discovery.rb', line 97 def should_expose_from_definition?(defn, , class_level, global_default) mcp_exposed = defn[:mcp_exposed] return mcp_exposed unless mcp_exposed.nil? should_expose?(, 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. } 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 |