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 +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/legion/mcp/context_compiler.rb +29 -3
- data/lib/legion/mcp/function_discovery.rb +75 -34
- data/lib/legion/mcp/tool_governance.rb +18 -2
- data/lib/legion/mcp/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a7b64b46eca9c059cbfa5fd95b354479914f48f22ecf109900cd3494a044ff3
|
|
4
|
+
data.tar.gz: 1877e520d1c814e6006247d1ff2745633aa866a13233829aa9a7b61a1082c87e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
#
|
|
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 =
|
|
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
|
-
|
|
45
|
-
return unless
|
|
45
|
+
defn = definition_for(runner_module, func_name)
|
|
46
|
+
return unless resolve_exposed(defn, meta, opts)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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(
|
|
88
|
-
runner_ref
|
|
89
|
-
func_ref
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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:
|
|
148
|
+
{ error: e.message }
|
|
110
149
|
end
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
data/lib/legion/mcp/version.rb
CHANGED