legion-mcp 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfce8f2adcb9e23e5ed0f4cbeef3cd1b9652b9e75e8ed9220ecbed03cd062a3b
4
- data.tar.gz: 1ea9a7387eed8be7ea87adee85017837306bca5abd41fdf546c3452e8e196ea3
3
+ metadata.gz: 4a7b64b46eca9c059cbfa5fd95b354479914f48f22ecf109900cd3494a044ff3
4
+ data.tar.gz: 1877e520d1c814e6006247d1ff2745633aa866a13233829aa9a7b61a1082c87e
5
5
  SHA512:
6
- metadata.gz: b8aa048bf6f665695e4588b31018ea849903a004149b56d0bd0032194c1cd474dc2086c2bb934205fbf85116ff90e58f6bfec9d8c524b270c4624c0607b56b0e
7
- data.tar.gz: 47bd47cf386c0b89ddc415abbf36aa5554ddad5cdd6165a937750f2307e46e19bda219bffda2bd9ce4faa6fd5be3db50b284f66b885684cc9e1c4c15e98534c6
6
+ metadata.gz: 4c616de66d0815a7fbfbbeebd1d3a803fa22e9f96a23aabdf5645b3abf0e59390473b0f773daf37a9abb412ff111d451b8fba207950606856e0aa661c7b859c7
7
+ data.tar.gz: b019a72aec60a83fdb7a0a7c13ef688166cca65ddebdb5e644e63c45da1501c31a6743569bf96de97e88e66184231e1894fa404453570bc70f8372fcb43063e8
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.4] - 2026-03-28
6
+
7
+ ### Changed
8
+ - `FunctionDiscovery`: prefer `definition[:mcp_exposed]` over deprecated `expose_as_mcp_tool` class method; fall back to legacy path when definition is absent
9
+ - `FunctionDiscovery#build_tool_class`: stores `mcp_category` and `mcp_tier` as singleton methods on dynamically built tool classes so downstream consumers can read them
10
+ - `ContextCompiler#compressed_catalog` and `#category_tools` now call `merged_categories`, which supplements the `CATEGORIES` constant with any tool classes that declare `mcp_category:` via the definition DSL
11
+ - `ToolGovernance#filter_tools`: prefers definition-level `mcp_tier` singleton method on the tool class over `DEFAULT_TOOL_TIERS` fallback; `DEFAULT_TOOL_TIERS` and `custom_tiers` (Settings) remain as fallbacks
12
+ - `FunctionDiscovery#register_function` refactored into `resolve_exposed` + `build_tool_opts` helpers to reduce perceived complexity
13
+
14
+ ### Added
15
+ - `FunctionDiscovery#definition_for` — reads `runner_module.definition_for(method)` if available, returns nil otherwise
16
+ - `FunctionDiscovery#should_expose_from_definition?` — exposure check that treats `definition[:mcp_exposed]` as highest precedence
17
+ - `FunctionDiscovery#build_tool_opts` — builds the options hash passed to `build_tool_class`
18
+ - `FunctionDiscovery#wire_call_method` — wires the `call` singleton method onto the built tool class
19
+ - `ContextCompiler#merged_categories` — merges `CATEGORIES` with definition-declared categories from registered tool classes
20
+ - `ToolGovernance#definition_tier` — extracts `mcp_tier` from a tool class singleton method (returns nil when absent)
21
+
5
22
  ## [0.6.3] - 2026-03-27
6
23
 
7
24
  ### Added
@@ -49,9 +49,12 @@ module Legion
49
49
  module_function
50
50
 
51
51
  # Returns a compressed summary of all categories with tool counts and tool name lists.
52
+ # Merges CATEGORIES (hardcoded fallback) with any categories declared via the definition DSL
53
+ # (mcp_category: on dynamically discovered tool classes).
52
54
  # @return [Array<Hash>] array of { category:, summary:, tool_count:, tools: }
53
55
  def compressed_catalog
54
- CATEGORIES.map do |category, config|
56
+ merged = merged_categories
57
+ merged.map do |category, config|
55
58
  tool_names = config[:tools]
56
59
  {
57
60
  category: category,
@@ -63,10 +66,11 @@ module Legion
63
66
  end
64
67
 
65
68
  # Returns tools for a specific category, filtered to only those present in Server.tool_registry.
66
- # @param category_sym [Symbol] one of the CATEGORIES keys
69
+ # Checks CATEGORIES (hardcoded fallback) as well as definition-declared mcp_category on tool classes.
70
+ # @param category_sym [Symbol] one of the CATEGORIES keys (or a definition-declared category)
67
71
  # @return [Hash, nil] { category:, summary:, tools: [{ name:, description:, params: }] } or nil
68
72
  def category_tools(category_sym)
69
- config = CATEGORIES[category_sym]
73
+ config = merged_categories[category_sym]
70
74
  return nil unless config
71
75
 
72
76
  index = tool_index
@@ -80,6 +84,28 @@ module Legion
80
84
  }
81
85
  end
82
86
 
87
+ # Builds a merged category map: CATEGORIES constant as fallback, augmented by tool classes
88
+ # that declare mcp_category: via the definition DSL.
89
+ # @return [Hash<Symbol, Hash>] { category_sym => { tools: [...], summary: '...' } }
90
+ def merged_categories
91
+ result = CATEGORIES.transform_values do |config|
92
+ { tools: config[:tools].dup, summary: config[:summary] }
93
+ end
94
+
95
+ Server.tool_registry.each do |klass|
96
+ next unless klass.respond_to?(:mcp_category) && klass.mcp_category
97
+
98
+ cat = klass.mcp_category.to_sym
99
+ if result.key?(cat)
100
+ result[cat][:tools] |= [klass.tool_name]
101
+ else
102
+ result[cat] = { tools: [klass.tool_name], summary: cat.to_s.tr('_', ' ').capitalize }
103
+ end
104
+ end
105
+
106
+ result
107
+ end
108
+
83
109
  # Keyword-match intent against tool names and descriptions.
84
110
  # @param intent_string [String] natural language intent
85
111
  # @return [Class, nil] best matching tool CLASS from Server.tool_registry or nil
@@ -34,6 +34,7 @@ module Legion
34
34
  end
35
35
 
36
36
  def runner_expose_opts(runner_module)
37
+ # @deprecated class_expose/prefix — prefer definition DSL (mcp_exposed:, mcp_tool_prefix:)
37
38
  class_expose = runner_module.respond_to?(:expose_as_mcp_tool) ? runner_module.expose_as_mcp_tool : nil
38
39
  global_expose = defined?(Legion::Settings) ? (Legion::Settings.dig(:mcp, :auto_expose_runners) || false) : false
39
40
  prefix = runner_module.respond_to?(:mcp_tool_prefix) ? runner_module.mcp_tool_prefix : nil
@@ -41,18 +42,50 @@ module Legion
41
42
  end
42
43
 
43
44
  def register_function(runner_module, func_name, meta, opts)
44
- return unless should_expose?(meta, opts[:class_expose], opts[:global_expose])
45
- return unless deps_satisfied?(meta[:requires])
45
+ defn = definition_for(runner_module, func_name)
46
+ return unless resolve_exposed(defn, meta, opts)
46
47
 
47
- tool_class = build_tool_class(
48
- name: derive_tool_name(func_name, opts[:prefix]),
49
- description: meta[:desc] || "Auto-discovered: #{func_name}",
48
+ requires = defn&.dig(:requires)&.map(&:to_s) || meta[:requires]
49
+ return unless deps_satisfied?(requires)
50
+
51
+ Server.register_tool(build_tool_class(build_tool_opts(runner_module, func_name, meta, opts, defn)))
52
+ end
53
+
54
+ def resolve_exposed(defn, meta, opts)
55
+ if defn.nil?
56
+ should_expose?(meta, opts[:class_expose], opts[:global_expose])
57
+ else
58
+ should_expose_from_definition?(defn, meta, opts[:class_expose], opts[:global_expose])
59
+ end
60
+ end
61
+
62
+ def build_tool_opts(runner_module, func_name, meta, opts, defn)
63
+ prefix = defn&.dig(:mcp_prefix) || opts[:prefix]
64
+ {
65
+ name: derive_tool_name(func_name, prefix),
66
+ description: meta[:desc] || defn&.dig(:desc) || "Auto-discovered: #{func_name}",
50
67
  input_schema: meta[:options] || { properties: {} },
51
68
  runner_module: runner_module,
52
- function_name: func_name
53
- )
69
+ function_name: func_name,
70
+ mcp_category: defn&.dig(:mcp_category),
71
+ mcp_tier: defn&.dig(:mcp_tier)
72
+ }
73
+ end
74
+
75
+ # Returns the definition hash for a method on a runner module, or nil if not available.
76
+ def definition_for(runner_module, func_name)
77
+ return nil unless runner_module.respond_to?(:definition_for)
78
+
79
+ runner_module.definition_for(func_name)
80
+ end
81
+
82
+ # Exposure check when a definition is present.
83
+ # definition[:mcp_exposed] takes highest precedence; falls back to legacy path.
84
+ def should_expose_from_definition?(defn, func_meta, class_level, global_default)
85
+ mcp_exposed = defn[:mcp_exposed]
86
+ return mcp_exposed unless mcp_exposed.nil?
54
87
 
55
- Server.register_tool(tool_class)
88
+ should_expose?(func_meta, class_level, global_default)
56
89
  end
57
90
 
58
91
  def should_expose?(func_meta, class_level, global_default)
@@ -84,34 +117,42 @@ module Legion
84
117
  end
85
118
  end
86
119
 
87
- def build_tool_class(name:, description:, input_schema:, runner_module:, function_name:)
88
- runner_ref = runner_module
89
- func_ref = function_name
90
-
91
- Class.new(::MCP::Tool) do
92
- tool_name name
93
- description description
94
- input_schema(input_schema)
95
-
96
- define_singleton_method(:call) do |**params|
97
- error = false
98
-
99
- result =
100
- if runner_ref.respond_to?(func_ref)
101
- begin
102
- runner_ref.public_send(func_ref, **params)
103
- rescue StandardError => e
104
- error = true
105
- { error: e.message }
106
- end
107
- else
120
+ def build_tool_class(opts)
121
+ runner_ref = opts[:runner_module]
122
+ func_ref = opts[:function_name]
123
+ tool_name_value = opts[:name]
124
+ description_value = opts[:description]
125
+ input_schema_value = opts[:input_schema]
126
+ mcp_category_value = opts[:mcp_category]
127
+ mcp_tier_value = opts[:mcp_tier]
128
+ klass = Class.new(::MCP::Tool) do
129
+ tool_name tool_name_value
130
+ description description_value
131
+ input_schema(input_schema_value)
132
+ define_singleton_method(:mcp_category) { mcp_category_value }
133
+ define_singleton_method(:mcp_tier) { mcp_tier_value }
134
+ end
135
+ wire_call_method(klass, runner_ref, func_ref)
136
+ klass
137
+ end
138
+
139
+ def wire_call_method(klass, runner_ref, func_ref)
140
+ klass.define_singleton_method(:call) do |**params|
141
+ error = false
142
+ result =
143
+ if runner_ref.respond_to?(func_ref)
144
+ begin
145
+ runner_ref.public_send(func_ref, **params)
146
+ rescue StandardError => e
108
147
  error = true
109
- { error: "function #{func_ref} not found" }
148
+ { error: e.message }
110
149
  end
111
-
112
- text = defined?(Legion::JSON) ? Legion::JSON.dump(result) : result.to_s
113
- ::MCP::Tool::Response.new([{ type: 'text', text: text }], error: error)
114
- end
150
+ else
151
+ error = true
152
+ { error: "function #{func_ref} not found" }
153
+ end
154
+ text = defined?(Legion::JSON) ? Legion::JSON.dump(result) : result.to_s
155
+ ::MCP::Tool::Response.new([{ type: 'text', text: text }], error: error)
115
156
  end
116
157
  end
117
158
  end
@@ -32,9 +32,11 @@ module Legion
32
32
  risk_tier = identity&.dig(:risk_tier) || :low
33
33
  tier_value = RISK_TIER_ORDER[risk_tier] || 0
34
34
 
35
- tool_tiers = DEFAULT_TOOL_TIERS.merge(custom_tiers)
35
+ # DEFAULT_TOOL_TIERS is the fallback; custom_tiers (from Settings) override it;
36
+ # definition-level mcp_tier on the tool class takes highest precedence.
37
+ fallback_tiers = DEFAULT_TOOL_TIERS.merge(custom_tiers)
36
38
  tools.select do |tool|
37
- tool_tier = tool_tiers[tool_name(tool)] || :low
39
+ tool_tier = definition_tier(tool) || fallback_tiers[tool_name(tool)] || :low
38
40
  (RISK_TIER_ORDER[tool_tier] || 0) <= tier_value
39
41
  end
40
42
  end
@@ -72,6 +74,20 @@ module Legion
72
74
  tool.to_s
73
75
  end
74
76
  end
77
+
78
+ # Returns the mcp_tier declared on the tool class via the definition DSL, or nil if absent.
79
+ # Tool classes built by FunctionDiscovery expose mcp_tier as a singleton method.
80
+ def definition_tier(tool)
81
+ return nil unless tool.respond_to?(:mcp_tier)
82
+
83
+ tier = tool.mcp_tier
84
+ return nil if tier.nil?
85
+
86
+ normalized = tier.to_s.downcase.to_sym
87
+ return nil unless RISK_TIER_ORDER.key?(normalized)
88
+
89
+ normalized
90
+ end
75
91
  end
76
92
  end
77
93
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.6.3'
5
+ VERSION = '0.6.4'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity