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/CyclomaticComplexity, Metrics/PerceivedComplexity.
-
.register_from_settings_extensions ⇒ Object
Registers tools from the centralized Settings::Extensions registry.
- .register_function(runner_module, func_name, meta, opts) ⇒ Object
- .reset_discovery! ⇒ Object
- .resolve_exposed(defn, meta, opts) ⇒ Object
- .runner_expose_opts(_runner_module) ⇒ Object
-
.settings_extensions_available? ⇒ Boolean
Returns true when Settings::Extensions is defined and has tools registered.
- .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
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/legion/mcp/function_discovery.rb', line 150 def build_tool_class(opts) log.debug("[mcp][discovery] action=build_tool_class tool=#{opts[:name]} " \ "category=#{opts[:mcp_category]} tier=#{opts[:mcp_tier]}") 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
92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/legion/mcp/function_discovery.rb', line 92 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
50 51 52 53 54 55 56 57 58 59 |
# File 'lib/legion/mcp/function_discovery.rb', line 50 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? log.debug("[mcp][discovery] action=build_tools_from_runner runner=#{runner_module} functions=#{functions.size}") 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.
106 107 108 109 110 |
# File 'lib/legion/mcp/function_discovery.rb', line 106 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
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/legion/mcp/function_discovery.rb', line 133 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
128 129 130 131 |
# File 'lib/legion/mcp/function_discovery.rb', line 128 def derive_tool_name(func_name, prefix) base = prefix || 'legion.generated' "#{base}.#{func_name}" end |
.discover_and_register ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity, 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 37 38 39 40 41 42 43 44 |
# File 'lib/legion/mcp/function_discovery.rb', line 10 def discover_and_register # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity return if @discovery_fired @discovery_fired = true log.debug('[mcp][discovery] action=discover_and_register') # Prefer centralized registry when available and populated if settings_extensions_available? log.debug('[mcp][discovery] action=discover_and_register source=settings_extensions') register_from_settings_extensions return end if defined?(Legion::Tools::Discovery) && Legion::Tools::Discovery.respond_to?(:discover_and_register) log.debug('[mcp][discovery] action=discover_and_register source=tools_discovery') 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') end end |
.register_from_settings_extensions ⇒ Object
Registers tools from the centralized Settings::Extensions registry. Each tool entry is adapted into an MCP tool class via ToolAdapter.
200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/legion/mcp/function_discovery.rb', line 200 def register_from_settings_extensions entries = Legion::Settings::Extensions.tools log.debug("[mcp][discovery] action=register_from_settings_extensions entries=#{entries.size}") entries.each do |tool_entry| next if Server.tool_registry.any? { |tc| tc.tool_name == tool_entry[:name] } adapter = ToolAdapter.from_registry_entry(tool_entry) Server.register_tool(adapter) if adapter rescue StandardError => e handle_exception(e, level: :debug, operation: 'legion.mcp.function_discovery.register_from_settings_extensions') end end |
.register_function(runner_module, func_name, meta, opts) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/legion/mcp/function_discovery.rb', line 66 def register_function(runner_module, func_name, , opts) defn = definition_for(runner_module, func_name) exposed = resolve_exposed(defn, , opts) unless exposed log.debug("[mcp][discovery] action=register_function func=#{func_name} skipped=not_exposed") return end requires = defn&.dig(:requires)&.map(&:to_s) || [:requires] unless deps_satisfied?(requires) log.debug("[mcp][discovery] action=register_function func=#{func_name} skipped=deps_unsatisfied") return end log.debug("[mcp][discovery] action=register_function func=#{func_name} runner=#{runner_module}") Server.register_tool(build_tool_class(build_tool_opts(runner_module, func_name, , opts, defn))) end |
.reset_discovery! ⇒ Object
46 47 48 |
# File 'lib/legion/mcp/function_discovery.rb', line 46 def reset_discovery! @discovery_fired = false end |
.resolve_exposed(defn, meta, opts) ⇒ Object
84 85 86 87 88 89 90 |
# File 'lib/legion/mcp/function_discovery.rb', line 84 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
61 62 63 64 |
# File 'lib/legion/mcp/function_discovery.rb', line 61 def runner_expose_opts(_runner_module) global_expose = Legion::Settings.dig(:mcp, :auto_expose_runners) || false { class_expose: nil, global_expose: global_expose, prefix: nil } end |
.settings_extensions_available? ⇒ Boolean
Returns true when Settings::Extensions is defined and has tools registered.
193 194 195 196 |
# File 'lib/legion/mcp/function_discovery.rb', line 193 def settings_extensions_available? Legion::Settings::Extensions.respond_to?(:tools) && Legion::Settings::Extensions.tools.any? end |
.should_expose?(func_meta, class_level, global_default) ⇒ Boolean
121 122 123 124 125 126 |
# File 'lib/legion/mcp/function_discovery.rb', line 121 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.
114 115 116 117 118 119 |
# File 'lib/legion/mcp/function_discovery.rb', line 114 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
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/legion/mcp/function_discovery.rb', line 171 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 = Legion::JSON.dump(result) ::MCP::Tool::Response.new([{ type: 'text', text: text }], error: error) end end |