legionio 1.4.71 → 1.4.72
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 +11 -0
- data/lib/legion/cli/observe_command.rb +94 -0
- data/lib/legion/cli.rb +5 -1
- data/lib/legion/mcp/observer.rb +135 -0
- data/lib/legion/mcp/server.rb +34 -0
- data/lib/legion/mcp/usage_filter.rb +86 -0
- data/lib/legion/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9c2f9048a25ea9cff5f0b8db43aaaf49ab40bb6c388b2f892c3f952fb7233f4
|
|
4
|
+
data.tar.gz: 02225b7029518b45f1d78f632cce4fdbaee631efb9c03cade5e575ac3c96e3cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 758ee671a15875e26a60fe87b10b3499c5ef47f766d5cd3d43498b7c362be4a791defe79b99bda56ccbf263558fa7a4e8450b307f3bc29734c3c3d279bba4dd4
|
|
7
|
+
data.tar.gz: 2c04803613fe2a2ba39ef4aa29865f66b96b237ba7c3f74920cd784bedfebc9bc90df91042714a016f06587a6d234ed722cf29a5727c039e8f09fbddfb322184
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.72] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- TBI Phase 0+2: MCP tool observation pipeline and usage-based filtering
|
|
7
|
+
- `Legion::MCP::Observer` module: in-memory tool call recording with counters, ring buffer, and intent tracking
|
|
8
|
+
- `Legion::MCP::UsageFilter` module: scores tools by frequency, recency, and keyword match; prunes dead tools
|
|
9
|
+
- MCP `instrumentation_callback` wiring: automatically records all `tools/call` invocations via Observer
|
|
10
|
+
- MCP `tools_list_handler` wiring: dynamically filters and ranks tools per-request based on usage data
|
|
11
|
+
- `legion observe` CLI command: `stats`, `recent`, `reset` subcommands for MCP tool usage inspection
|
|
12
|
+
- 96 new specs covering Observer, UsageFilter, CLI command, and integration wiring
|
|
13
|
+
|
|
3
14
|
## [1.4.71] - 2026-03-19
|
|
4
15
|
|
|
5
16
|
### Added
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'legion/mcp/observer'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module CLI
|
|
8
|
+
class ObserveCommand < Thor
|
|
9
|
+
namespace :observe
|
|
10
|
+
|
|
11
|
+
desc 'stats', 'Show MCP tool usage statistics'
|
|
12
|
+
def stats
|
|
13
|
+
data = Legion::MCP::Observer.stats
|
|
14
|
+
|
|
15
|
+
if options['json']
|
|
16
|
+
puts ::JSON.pretty_generate(serialize_stats(data))
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
puts 'MCP Tool Observation Stats'
|
|
21
|
+
puts '=' * 40
|
|
22
|
+
puts "Total Calls: #{data[:total_calls]}"
|
|
23
|
+
puts "Tools Used: #{data[:tool_count]}"
|
|
24
|
+
puts "Failure Rate: #{(data[:failure_rate] * 100).round(1)}%"
|
|
25
|
+
puts "Since: #{data[:since]&.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
26
|
+
puts
|
|
27
|
+
|
|
28
|
+
return if data[:top_tools].empty?
|
|
29
|
+
|
|
30
|
+
puts 'Top Tools:'
|
|
31
|
+
puts '-' * 60
|
|
32
|
+
puts 'Tool Calls Avg(ms) Fails'
|
|
33
|
+
puts '-' * 60
|
|
34
|
+
data[:top_tools].each do |tool|
|
|
35
|
+
puts format('%-30<name>s %6<calls>d %8<avg>d %6<fails>d',
|
|
36
|
+
name: tool[:name], calls: tool[:call_count],
|
|
37
|
+
avg: tool[:avg_latency_ms], fails: tool[:failure_count])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc 'recent', 'Show recent MCP tool calls'
|
|
42
|
+
method_option :limit, type: :numeric, default: 20, aliases: '-n'
|
|
43
|
+
def recent
|
|
44
|
+
calls = Legion::MCP::Observer.recent(options['limit'] || 20)
|
|
45
|
+
|
|
46
|
+
if options['json']
|
|
47
|
+
puts ::JSON.pretty_generate(calls.map { |c| serialize_call(c) })
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if calls.empty?
|
|
52
|
+
puts 'No recent tool calls recorded.'
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts 'Tool Duration Status Time'
|
|
57
|
+
puts '-' * 70
|
|
58
|
+
calls.reverse_each do |call|
|
|
59
|
+
status = call[:success] ? 'OK' : 'FAIL'
|
|
60
|
+
time = call[:timestamp]&.strftime('%H:%M:%S')
|
|
61
|
+
puts format('%-30<tool>s %6<dur>dms %7<st>s %<tm>s',
|
|
62
|
+
tool: call[:tool_name], dur: call[:duration_ms], st: status, tm: time)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc 'reset', 'Clear all observation data'
|
|
67
|
+
def reset
|
|
68
|
+
print 'Clear all observation data? (yes/no): '
|
|
69
|
+
return unless $stdin.gets&.strip&.downcase == 'yes'
|
|
70
|
+
|
|
71
|
+
Legion::MCP::Observer.reset!
|
|
72
|
+
puts 'Observation data cleared.'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def serialize_stats(data)
|
|
78
|
+
{
|
|
79
|
+
total_calls: data[:total_calls],
|
|
80
|
+
tool_count: data[:tool_count],
|
|
81
|
+
failure_rate: data[:failure_rate],
|
|
82
|
+
since: data[:since]&.iso8601,
|
|
83
|
+
top_tools: data[:top_tools].map { |t| t.transform_keys(&:to_s) }
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def serialize_call(call)
|
|
88
|
+
call.transform_keys(&:to_s).tap do |c|
|
|
89
|
+
c['timestamp'] = c['timestamp']&.iso8601 if c['timestamp']
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -43,7 +43,8 @@ module Legion
|
|
|
43
43
|
autoload :Marketplace, 'legion/cli/marketplace_command'
|
|
44
44
|
autoload :Notebook, 'legion/cli/notebook_command'
|
|
45
45
|
autoload :Llm, 'legion/cli/llm_command'
|
|
46
|
-
autoload :Tty,
|
|
46
|
+
autoload :Tty, 'legion/cli/tty_command'
|
|
47
|
+
autoload :ObserveCommand, 'legion/cli/observe_command'
|
|
47
48
|
autoload :Interactive, 'legion/cli/interactive'
|
|
48
49
|
|
|
49
50
|
class Main < Thor
|
|
@@ -241,6 +242,9 @@ module Legion
|
|
|
241
242
|
desc 'tty', 'Rich terminal UI (onboarding, AI chat, dashboard)'
|
|
242
243
|
subcommand 'tty', Legion::CLI::Tty
|
|
243
244
|
|
|
245
|
+
desc 'observe SUBCOMMAND', 'MCP tool observation stats'
|
|
246
|
+
subcommand 'observe', Legion::CLI::ObserveCommand
|
|
247
|
+
|
|
244
248
|
desc 'tree', 'Print a tree of all available commands'
|
|
245
249
|
def tree
|
|
246
250
|
legion_print_command_tree(self.class, 'legion', '')
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent-ruby'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module MCP
|
|
7
|
+
module Observer
|
|
8
|
+
RING_BUFFER_MAX = 500
|
|
9
|
+
INTENT_BUFFER_MAX = 200
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def record(tool_name:, duration_ms:, success:, params_keys: [], error: nil)
|
|
14
|
+
now = Time.now
|
|
15
|
+
|
|
16
|
+
counters_mutex.synchronize do
|
|
17
|
+
entry = counters[tool_name] || { call_count: 0, total_latency_ms: 0.0, failure_count: 0,
|
|
18
|
+
last_used: nil, last_error: nil }
|
|
19
|
+
counters[tool_name] = {
|
|
20
|
+
call_count: entry[:call_count] + 1,
|
|
21
|
+
total_latency_ms: entry[:total_latency_ms] + duration_ms.to_f,
|
|
22
|
+
failure_count: entry[:failure_count] + (success ? 0 : 1),
|
|
23
|
+
last_used: now,
|
|
24
|
+
last_error: success ? entry[:last_error] : error
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
buffer_mutex.synchronize do
|
|
29
|
+
ring_buffer << {
|
|
30
|
+
tool_name: tool_name,
|
|
31
|
+
duration_ms: duration_ms,
|
|
32
|
+
success: success,
|
|
33
|
+
params_keys: params_keys,
|
|
34
|
+
error: error,
|
|
35
|
+
recorded_at: now
|
|
36
|
+
}
|
|
37
|
+
ring_buffer.shift if ring_buffer.size > RING_BUFFER_MAX
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def record_intent(intent, matched_tool_name)
|
|
42
|
+
intent_mutex.synchronize do
|
|
43
|
+
intent_buffer << { intent: intent, matched_tool: matched_tool_name, recorded_at: Time.now }
|
|
44
|
+
intent_buffer.shift if intent_buffer.size > INTENT_BUFFER_MAX
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tool_stats(tool_name)
|
|
49
|
+
entry = counters_mutex.synchronize { counters[tool_name] }
|
|
50
|
+
return nil unless entry
|
|
51
|
+
|
|
52
|
+
count = entry[:call_count]
|
|
53
|
+
avg = count.positive? ? (entry[:total_latency_ms] / count).round(2) : 0.0
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
name: tool_name,
|
|
57
|
+
call_count: count,
|
|
58
|
+
avg_latency_ms: avg,
|
|
59
|
+
failure_count: entry[:failure_count],
|
|
60
|
+
last_used: entry[:last_used],
|
|
61
|
+
last_error: entry[:last_error]
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def all_tool_stats
|
|
66
|
+
names = counters_mutex.synchronize { counters.keys.dup }
|
|
67
|
+
names.to_h { |name| [name, tool_stats(name)] }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stats
|
|
71
|
+
all_names = counters_mutex.synchronize { counters.keys.dup }
|
|
72
|
+
total = all_names.sum { |n| counters_mutex.synchronize { counters[n][:call_count] } }
|
|
73
|
+
failures = all_names.sum { |n| counters_mutex.synchronize { counters[n][:failure_count] } }
|
|
74
|
+
rate = total.positive? ? (failures.to_f / total).round(4) : 0.0
|
|
75
|
+
|
|
76
|
+
top = all_names
|
|
77
|
+
.map { |n| tool_stats(n) }
|
|
78
|
+
.sort_by { |s| -s[:call_count] }
|
|
79
|
+
.first(10)
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
total_calls: total,
|
|
83
|
+
tool_count: all_names.size,
|
|
84
|
+
failure_rate: rate,
|
|
85
|
+
top_tools: top,
|
|
86
|
+
since: started_at
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def recent(limit = 10)
|
|
91
|
+
buffer_mutex.synchronize { ring_buffer.last(limit) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def recent_intents(limit = 10)
|
|
95
|
+
intent_mutex.synchronize { intent_buffer.last(limit) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def reset!
|
|
99
|
+
counters_mutex.synchronize { counters.clear }
|
|
100
|
+
buffer_mutex.synchronize { ring_buffer.clear }
|
|
101
|
+
intent_mutex.synchronize { intent_buffer.clear }
|
|
102
|
+
@started_at = Time.now
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Internal state accessors
|
|
106
|
+
def counters
|
|
107
|
+
@counters ||= {}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def counters_mutex
|
|
111
|
+
@counters_mutex ||= Mutex.new
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def ring_buffer
|
|
115
|
+
@ring_buffer ||= []
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def buffer_mutex
|
|
119
|
+
@buffer_mutex ||= Mutex.new
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def intent_buffer
|
|
123
|
+
@intent_buffer ||= []
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def intent_mutex
|
|
127
|
+
@intent_mutex ||= Mutex.new
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def started_at
|
|
131
|
+
@started_at ||= Time.now
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
data/lib/legion/mcp/server.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'observer'
|
|
4
|
+
require_relative 'usage_filter'
|
|
3
5
|
require_relative 'tools/run_task'
|
|
4
6
|
require_relative 'tools/describe_runner'
|
|
5
7
|
require_relative 'tools/list_tasks'
|
|
@@ -97,12 +99,44 @@ module Legion
|
|
|
97
99
|
resource_templates: Resources::ExtensionInfo.resource_templates
|
|
98
100
|
)
|
|
99
101
|
|
|
102
|
+
if defined?(Observer)
|
|
103
|
+
::MCP.configure do |c|
|
|
104
|
+
c.instrumentation_callback = ->(idata) { Server.wire_observer(idata) }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
server.tools_list_handler do |_params|
|
|
109
|
+
build_filtered_tool_list.map(&:to_h)
|
|
110
|
+
end
|
|
111
|
+
|
|
100
112
|
Resources::RunnerCatalog.register(server)
|
|
101
113
|
Resources::ExtensionInfo.register_read_handler(server)
|
|
102
114
|
|
|
103
115
|
server
|
|
104
116
|
end
|
|
105
117
|
|
|
118
|
+
def wire_observer(data)
|
|
119
|
+
return unless data[:method] == 'tools/call' && data[:tool_name]
|
|
120
|
+
|
|
121
|
+
duration_ms = (data[:duration].to_f * 1000).to_i
|
|
122
|
+
params_keys = data[:tool_arguments].respond_to?(:keys) ? data[:tool_arguments].keys : []
|
|
123
|
+
success = data[:error].nil?
|
|
124
|
+
|
|
125
|
+
Observer.record(
|
|
126
|
+
tool_name: data[:tool_name],
|
|
127
|
+
duration_ms: duration_ms,
|
|
128
|
+
success: success,
|
|
129
|
+
params_keys: params_keys,
|
|
130
|
+
error: data[:error]
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def build_filtered_tool_list(keywords: [])
|
|
135
|
+
tool_names = TOOL_CLASSES.map { |tc| tc.respond_to?(:tool_name) ? tc.tool_name : tc.name }
|
|
136
|
+
ranked = UsageFilter.ranked_tools(tool_names, keywords: keywords)
|
|
137
|
+
ranked.filter_map { |name| TOOL_CLASSES.find { |tc| (tc.respond_to?(:tool_name) ? tc.tool_name : tc.name) == name } }
|
|
138
|
+
end
|
|
139
|
+
|
|
106
140
|
private
|
|
107
141
|
|
|
108
142
|
def instructions
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module UsageFilter
|
|
6
|
+
ESSENTIAL_TOOLS = %w[
|
|
7
|
+
legion.do legion.tools legion.run_task legion.get_status legion.describe_runner
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
FREQUENCY_WEIGHT = 0.5
|
|
11
|
+
RECENCY_WEIGHT = 0.3
|
|
12
|
+
KEYWORD_WEIGHT = 0.2
|
|
13
|
+
BASELINE_SCORE = 0.1
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def score_tools(tool_names, keywords: [])
|
|
18
|
+
all_stats = Observer.all_tool_stats
|
|
19
|
+
call_counts = tool_names.map { |n| all_stats.dig(n, :call_count) || 0 }
|
|
20
|
+
max_calls = call_counts.max || 0
|
|
21
|
+
|
|
22
|
+
tool_names.each_with_object({}) do |name, hash|
|
|
23
|
+
stats = all_stats[name]
|
|
24
|
+
|
|
25
|
+
freq_score = if max_calls.positive? && stats
|
|
26
|
+
(stats[:call_count].to_f / max_calls) * FREQUENCY_WEIGHT
|
|
27
|
+
else
|
|
28
|
+
0.0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
rec_score = if stats&.dig(:last_used)
|
|
32
|
+
recency_decay(stats[:last_used]) * RECENCY_WEIGHT
|
|
33
|
+
else
|
|
34
|
+
0.0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
kw_score = keyword_match(name, keywords) * KEYWORD_WEIGHT
|
|
38
|
+
|
|
39
|
+
total = freq_score + rec_score + kw_score
|
|
40
|
+
total = BASELINE_SCORE if total.zero?
|
|
41
|
+
|
|
42
|
+
hash[name] = total.round(6)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ranked_tools(tool_names, limit: nil, keywords: [])
|
|
47
|
+
scores = score_tools(tool_names, keywords: keywords)
|
|
48
|
+
ranked = tool_names.sort_by { |n| -scores.fetch(n, BASELINE_SCORE) }
|
|
49
|
+
limit ? ranked.first(limit) : ranked
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def prune_dead_tools(tool_names, prune_after_seconds: 86_400 * 30)
|
|
53
|
+
stats = Observer.stats
|
|
54
|
+
window = stats[:since]
|
|
55
|
+
elapsed = window ? (Time.now - window) : 0
|
|
56
|
+
|
|
57
|
+
return tool_names if elapsed < prune_after_seconds
|
|
58
|
+
|
|
59
|
+
all_stats = Observer.all_tool_stats
|
|
60
|
+
tool_names.reject do |name|
|
|
61
|
+
next false if ESSENTIAL_TOOLS.include?(name)
|
|
62
|
+
|
|
63
|
+
calls = all_stats.dig(name, :call_count) || 0
|
|
64
|
+
calls.zero?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def recency_decay(last_used)
|
|
69
|
+
return 0.0 unless last_used
|
|
70
|
+
|
|
71
|
+
age_seconds = Time.now - last_used
|
|
72
|
+
return 1.0 if age_seconds <= 0
|
|
73
|
+
|
|
74
|
+
decay = 1.0 - (age_seconds / 86_400.0)
|
|
75
|
+
decay.clamp(0.0, 1.0)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def keyword_match(tool_name, keywords)
|
|
79
|
+
return 0.0 if keywords.nil? || keywords.empty?
|
|
80
|
+
|
|
81
|
+
hits = keywords.count { |kw| tool_name.include?(kw.to_s) }
|
|
82
|
+
hits.to_f / keywords.size
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.72
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -490,6 +490,7 @@ files:
|
|
|
490
490
|
- lib/legion/cli/mcp_command.rb
|
|
491
491
|
- lib/legion/cli/memory_command.rb
|
|
492
492
|
- lib/legion/cli/notebook_command.rb
|
|
493
|
+
- lib/legion/cli/observe_command.rb
|
|
493
494
|
- lib/legion/cli/openapi_command.rb
|
|
494
495
|
- lib/legion/cli/output.rb
|
|
495
496
|
- lib/legion/cli/plan_command.rb
|
|
@@ -559,6 +560,7 @@ files:
|
|
|
559
560
|
- lib/legion/mcp.rb
|
|
560
561
|
- lib/legion/mcp/auth.rb
|
|
561
562
|
- lib/legion/mcp/context_compiler.rb
|
|
563
|
+
- lib/legion/mcp/observer.rb
|
|
562
564
|
- lib/legion/mcp/resources/extension_info.rb
|
|
563
565
|
- lib/legion/mcp/resources/runner_catalog.rb
|
|
564
566
|
- lib/legion/mcp/server.rb
|
|
@@ -598,6 +600,7 @@ files:
|
|
|
598
600
|
- lib/legion/mcp/tools/update_schedule.rb
|
|
599
601
|
- lib/legion/mcp/tools/worker_costs.rb
|
|
600
602
|
- lib/legion/mcp/tools/worker_lifecycle.rb
|
|
603
|
+
- lib/legion/mcp/usage_filter.rb
|
|
601
604
|
- lib/legion/metrics.rb
|
|
602
605
|
- lib/legion/process.rb
|
|
603
606
|
- lib/legion/readiness.rb
|