legion-mcp 0.5.3 → 0.5.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 +11 -0
- data/lib/legion/mcp/function_generator.rb +158 -0
- data/lib/legion/mcp/gap_detector.rb +78 -25
- data/lib/legion/mcp/self_generate.rb +111 -0
- data/lib/legion/mcp/server.rb +3 -0
- data/lib/legion/mcp/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c56ad22d31474ed9a1a64e6b97a4aea17a61a496162916a0d1f3c727c82abd2
|
|
4
|
+
data.tar.gz: 1d1ebbc6f7dcbcbf26a0a42fa7d92694b6b2f24dd7518089fe8132919d00a6f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fa820c5e258c9fd3690d991b00aab73568c2e72189f5adf7511a264a10f9287f3ca87b96d2091bdb7a5fec8876fe5a54986f7d1067cf9e72c95b20f884039c7
|
|
7
|
+
data.tar.gz: 1f4111a18475467b942d3faa2628c23ac77ea0c4b7165b78dcc7c222908d7af97ae31eb3f58e610c598413238713d1c5f96fa6fee8201c7fc0144ba36165c336
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# legion-mcp Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.4] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- TBI Phase 5: `GapDetector` — detects unmatched intents, high-failure tools, and stale candidates from Observer/PatternStore data
|
|
7
|
+
- TBI Phase 5: `FunctionGenerator` — LLM-powered tool spec generation from detected gaps, with validation and pattern registration
|
|
8
|
+
- TBI Phase 5: `SelfGenerate` — orchestrates gap detection + function generation cycles with cooldown, history tracking, and status reporting
|
|
9
|
+
- 89 new specs across gap_detector, function_generator, and self_generate
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Rewrote `GapDetector` from frequency-based to gap-type-based detection (unmatched, failure, stale) with priority scoring
|
|
13
|
+
|
|
3
14
|
## [0.5.3] - 2026-03-23
|
|
4
15
|
|
|
5
16
|
### Changed
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module MCP
|
|
7
|
+
module FunctionGenerator
|
|
8
|
+
MAX_GENERATION_ATTEMPTS = 3
|
|
9
|
+
GENERATION_TIMEOUT = 60
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def generate_from_gap(gap)
|
|
14
|
+
case gap[:type]
|
|
15
|
+
when :unmatched_intent
|
|
16
|
+
generate_tool_for_intent(intent: gap[:intent])
|
|
17
|
+
when :high_failure_tool
|
|
18
|
+
generate_fix_for_tool(tool_name: gap[:tool_name], last_error: gap[:last_error])
|
|
19
|
+
when :stale_candidate
|
|
20
|
+
generate_tool_for_candidate(intent_text: gap[:intent_text], tool_chain: gap[:tool_chain])
|
|
21
|
+
else
|
|
22
|
+
{ success: false, reason: :unknown_gap_type }
|
|
23
|
+
end
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
{ success: false, reason: :generation_failed, error: e.message }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate_tool_for_intent(intent:)
|
|
29
|
+
return { success: false, reason: :llm_not_available } unless llm_available?
|
|
30
|
+
|
|
31
|
+
spec = generate_tool_spec(intent: intent)
|
|
32
|
+
return spec unless spec[:success]
|
|
33
|
+
|
|
34
|
+
validate_spec(spec[:tool_spec])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate_fix_for_tool(tool_name:, last_error:)
|
|
38
|
+
return { success: false, reason: :llm_not_available } unless llm_available?
|
|
39
|
+
|
|
40
|
+
prompt = build_fix_prompt(tool_name: tool_name, error: last_error)
|
|
41
|
+
result = llm_ask(prompt)
|
|
42
|
+
return { success: false, reason: :llm_failed } unless result
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
success: true,
|
|
46
|
+
type: :fix_suggestion,
|
|
47
|
+
tool_name: tool_name,
|
|
48
|
+
suggestion: result,
|
|
49
|
+
requires_review: true
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_tool_for_candidate(intent_text:, tool_chain:)
|
|
54
|
+
return { success: false, reason: :llm_not_available } unless llm_available?
|
|
55
|
+
|
|
56
|
+
spec = generate_tool_spec(intent: intent_text, existing_chain: tool_chain)
|
|
57
|
+
return spec unless spec[:success]
|
|
58
|
+
|
|
59
|
+
register_generated_pattern(spec[:tool_spec], intent_text)
|
|
60
|
+
|
|
61
|
+
spec
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def generate_tool_spec(intent:, existing_chain: nil)
|
|
65
|
+
prompt = build_generation_prompt(intent: intent, existing_chain: existing_chain)
|
|
66
|
+
result = llm_ask(prompt)
|
|
67
|
+
return { success: false, reason: :llm_failed } unless result
|
|
68
|
+
|
|
69
|
+
parsed = parse_tool_spec(result)
|
|
70
|
+
return { success: false, reason: :parse_failed, raw: result } unless parsed
|
|
71
|
+
|
|
72
|
+
{ success: true, tool_spec: parsed }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_spec(spec)
|
|
76
|
+
errors = []
|
|
77
|
+
errors << 'missing name' unless spec[:name]&.length&.positive?
|
|
78
|
+
errors << 'missing description' unless spec[:description]&.length&.positive?
|
|
79
|
+
errors << 'missing runner_function' unless spec[:runner_function]&.length&.positive?
|
|
80
|
+
|
|
81
|
+
if errors.empty?
|
|
82
|
+
{ success: true, tool_spec: spec, valid: true }
|
|
83
|
+
else
|
|
84
|
+
{ success: false, reason: :invalid_spec, errors: errors, tool_spec: spec }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def llm_available?
|
|
89
|
+
!!(defined?(Legion::LLM) && Legion::LLM.respond_to?(:chat))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def llm_ask(prompt)
|
|
93
|
+
return nil unless llm_available?
|
|
94
|
+
|
|
95
|
+
response = Legion::LLM.chat(
|
|
96
|
+
message: prompt,
|
|
97
|
+
caller: { source: 'legion-mcp', component: 'function_generator' }
|
|
98
|
+
)
|
|
99
|
+
response&.content
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
Legion::Logging.warn("FunctionGenerator LLM call failed: #{e.message}") if defined?(Legion::Logging)
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_generation_prompt(intent:, existing_chain: nil)
|
|
106
|
+
chain_context = existing_chain ? "\nExisting tool chain that partially handles this: #{existing_chain.inspect}" : ''
|
|
107
|
+
|
|
108
|
+
<<~PROMPT
|
|
109
|
+
Generate a tool specification for a LegionIO MCP tool that handles this user intent:
|
|
110
|
+
"#{intent}"
|
|
111
|
+
#{chain_context}
|
|
112
|
+
Respond with ONLY a JSON object (no markdown, no explanation):
|
|
113
|
+
{
|
|
114
|
+
"name": "legion.tool_name",
|
|
115
|
+
"description": "What this tool does",
|
|
116
|
+
"runner_function": "extension_name/runner_name/method_name",
|
|
117
|
+
"parameters": [{"name": "param1", "type": "string", "required": true, "description": "..."}],
|
|
118
|
+
"category": "one of: query, action, analysis, utility"
|
|
119
|
+
}
|
|
120
|
+
PROMPT
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_fix_prompt(tool_name:, error:)
|
|
124
|
+
<<~PROMPT
|
|
125
|
+
The MCP tool "#{tool_name}" has a high failure rate. Last error: #{error}
|
|
126
|
+
Suggest a fix or replacement approach. Respond concisely (2-3 sentences max).
|
|
127
|
+
PROMPT
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_tool_spec(raw)
|
|
131
|
+
json_match = raw.match(/\{[\s\S]*\}/)
|
|
132
|
+
return nil unless json_match
|
|
133
|
+
|
|
134
|
+
parsed = ::JSON.parse(json_match[0], symbolize_names: true)
|
|
135
|
+
return nil unless parsed.is_a?(Hash) && parsed[:name]
|
|
136
|
+
|
|
137
|
+
parsed
|
|
138
|
+
rescue ::JSON::ParserError
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def register_generated_pattern(spec, intent_text)
|
|
143
|
+
return unless defined?(PatternStore)
|
|
144
|
+
|
|
145
|
+
normalized = intent_text.to_s.strip.downcase.gsub(/\s+/, ' ')
|
|
146
|
+
intent_hash = Digest::SHA256.hexdigest(normalized)
|
|
147
|
+
|
|
148
|
+
PatternStore.promote_candidate(
|
|
149
|
+
intent_hash: intent_hash,
|
|
150
|
+
tool_chain: [spec[:runner_function] || spec[:name]],
|
|
151
|
+
intent_text: intent_text
|
|
152
|
+
)
|
|
153
|
+
rescue StandardError => e
|
|
154
|
+
Legion::Logging.warn("register_generated_pattern failed: #{e.message}") if defined?(Legion::Logging)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -5,48 +5,101 @@ require 'digest'
|
|
|
5
5
|
module Legion
|
|
6
6
|
module MCP
|
|
7
7
|
module GapDetector
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
GAP_INTENT_THRESHOLD = 5
|
|
9
|
+
FAILURE_RATE_THRESHOLD = 0.4
|
|
10
|
+
STALE_CANDIDATE_HOURS = 24
|
|
11
|
+
MAX_GAPS = 20
|
|
10
12
|
|
|
11
13
|
module_function
|
|
12
14
|
|
|
13
|
-
def
|
|
15
|
+
def detect_gaps
|
|
14
16
|
gaps = []
|
|
15
|
-
gaps.concat(
|
|
16
|
-
gaps.concat(
|
|
17
|
-
gaps
|
|
17
|
+
gaps.concat(detect_unmatched_intents)
|
|
18
|
+
gaps.concat(detect_high_failure_tools)
|
|
19
|
+
gaps.concat(detect_stale_candidates)
|
|
20
|
+
|
|
21
|
+
gaps.uniq { |g| g[:id] }.first(MAX_GAPS)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
def detect_unmatched_intents
|
|
25
|
+
return [] unless defined?(Observer)
|
|
26
|
+
|
|
27
|
+
recent = Observer.recent_intents(200)
|
|
28
|
+
unmatched = recent.select { |r| r[:matched_tool].nil? || r[:matched_tool] == 'none' }
|
|
29
|
+
|
|
30
|
+
grouped = unmatched.group_by { |r| normalize_intent(r[:intent]) }
|
|
23
31
|
|
|
24
|
-
grouped.filter_map do |
|
|
25
|
-
next if occurrences.size <
|
|
26
|
-
next if PatternStore.pattern_exists?(Digest::SHA256.hexdigest(tool.to_s))
|
|
32
|
+
grouped.filter_map do |intent_text, occurrences|
|
|
33
|
+
next if occurrences.size < GAP_INTENT_THRESHOLD
|
|
27
34
|
|
|
28
|
-
{
|
|
29
|
-
|
|
35
|
+
{
|
|
36
|
+
id: "unmatched:#{Digest::SHA256.hexdigest(intent_text)[0, 12]}",
|
|
37
|
+
type: :unmatched_intent,
|
|
38
|
+
intent: intent_text,
|
|
39
|
+
occurrences: occurrences.size,
|
|
40
|
+
first_seen: occurrences.first[:recorded_at],
|
|
41
|
+
last_seen: occurrences.last[:recorded_at],
|
|
42
|
+
priority: calculate_priority(occurrences.size, :unmatched)
|
|
43
|
+
}
|
|
30
44
|
end
|
|
31
45
|
end
|
|
32
46
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
def detect_high_failure_tools
|
|
48
|
+
return [] unless defined?(Observer)
|
|
49
|
+
|
|
50
|
+
stats = Observer.all_tool_stats
|
|
51
|
+
stats.filter_map do |tool_name, tool_stat|
|
|
52
|
+
next unless tool_stat
|
|
53
|
+
next if tool_stat[:call_count] < 5
|
|
54
|
+
|
|
55
|
+
failure_rate = tool_stat[:failure_count].to_f / tool_stat[:call_count]
|
|
56
|
+
next if failure_rate < FAILURE_RATE_THRESHOLD
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
id: "failing:#{tool_name}",
|
|
60
|
+
type: :high_failure_tool,
|
|
61
|
+
tool_name: tool_name,
|
|
62
|
+
failure_rate: failure_rate.round(4),
|
|
63
|
+
call_count: tool_stat[:call_count],
|
|
64
|
+
failure_count: tool_stat[:failure_count],
|
|
65
|
+
last_error: tool_stat[:last_error],
|
|
66
|
+
priority: calculate_priority(tool_stat[:failure_count], :failure)
|
|
67
|
+
}
|
|
39
68
|
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def detect_stale_candidates
|
|
72
|
+
return [] unless defined?(PatternStore)
|
|
73
|
+
|
|
74
|
+
candidates = PatternStore.candidates
|
|
40
75
|
|
|
41
|
-
|
|
42
|
-
next if count <
|
|
76
|
+
candidates.filter_map do |intent_hash, entry|
|
|
77
|
+
next if entry[:count] < 2
|
|
43
78
|
|
|
44
|
-
{
|
|
79
|
+
{
|
|
80
|
+
id: "stale:#{intent_hash[0, 12]}",
|
|
81
|
+
type: :stale_candidate,
|
|
82
|
+
intent_hash: intent_hash,
|
|
83
|
+
intent_text: entry[:intent_text],
|
|
84
|
+
observation_count: entry[:count],
|
|
85
|
+
tool_chain: entry[:tool_chain],
|
|
86
|
+
priority: calculate_priority(entry[:count], :stale)
|
|
87
|
+
}
|
|
45
88
|
end
|
|
46
89
|
end
|
|
47
90
|
|
|
48
|
-
def
|
|
49
|
-
|
|
91
|
+
def normalize_intent(text)
|
|
92
|
+
text.to_s.strip.downcase.gsub(/\s+/, ' ')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def calculate_priority(count, type)
|
|
96
|
+
base = case type
|
|
97
|
+
when :unmatched then 0.8
|
|
98
|
+
when :failure then 0.6
|
|
99
|
+
when :stale then 0.4
|
|
100
|
+
else 0.3
|
|
101
|
+
end
|
|
102
|
+
(base + [count * 0.02, 0.2].min).clamp(0.0, 1.0).round(4)
|
|
50
103
|
end
|
|
51
104
|
end
|
|
52
105
|
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module MCP
|
|
7
|
+
module SelfGenerate
|
|
8
|
+
MAX_GAPS_PER_CYCLE = 5
|
|
9
|
+
COOLDOWN_SECONDS = 300
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def run_cycle
|
|
14
|
+
return { success: false, reason: :cooldown } if in_cooldown?
|
|
15
|
+
|
|
16
|
+
gaps = GapDetector.detect_gaps
|
|
17
|
+
return { success: true, gaps_found: 0, generated: 0 } if gaps.empty?
|
|
18
|
+
|
|
19
|
+
top_gaps = gaps.sort_by { |g| -g[:priority] }.first(MAX_GAPS_PER_CYCLE)
|
|
20
|
+
|
|
21
|
+
results = top_gaps.map do |gap|
|
|
22
|
+
result = FunctionGenerator.generate_from_gap(gap)
|
|
23
|
+
{ gap: gap[:id], type: gap[:type], result: result }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
record_cycle(results)
|
|
27
|
+
|
|
28
|
+
generated = results.count { |r| r[:result][:success] }
|
|
29
|
+
failed = results.count { |r| !r[:result][:success] }
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
success: true,
|
|
33
|
+
gaps_found: gaps.size,
|
|
34
|
+
processed: top_gaps.size,
|
|
35
|
+
generated: generated,
|
|
36
|
+
failed: failed,
|
|
37
|
+
results: results
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def status
|
|
42
|
+
{
|
|
43
|
+
last_cycle_at: last_cycle_at,
|
|
44
|
+
total_cycles: cycle_count,
|
|
45
|
+
total_generated: total_generated,
|
|
46
|
+
cooldown_remaining: cooldown_remaining,
|
|
47
|
+
pending_gaps: GapDetector.detect_gaps.size
|
|
48
|
+
}
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
{ error: e.message }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reset!
|
|
54
|
+
mutex.synchronize do
|
|
55
|
+
@last_cycle_at = nil
|
|
56
|
+
@cycle_count = 0
|
|
57
|
+
@total_generated = 0
|
|
58
|
+
@cycle_history = []
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cycle_history(limit = 10)
|
|
63
|
+
mutex.synchronize { (@cycle_history || []).last(limit) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def in_cooldown?
|
|
67
|
+
return false unless last_cycle_at
|
|
68
|
+
|
|
69
|
+
Time.now - last_cycle_at < COOLDOWN_SECONDS
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cooldown_remaining
|
|
73
|
+
return 0 unless last_cycle_at
|
|
74
|
+
|
|
75
|
+
remaining = COOLDOWN_SECONDS - (Time.now - last_cycle_at)
|
|
76
|
+
[remaining, 0].max.round(1)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def record_cycle(results)
|
|
80
|
+
mutex.synchronize do
|
|
81
|
+
@last_cycle_at = Time.now
|
|
82
|
+
@cycle_count = (@cycle_count || 0) + 1
|
|
83
|
+
@total_generated = (@total_generated || 0) + results.count { |r| r[:result][:success] }
|
|
84
|
+
@cycle_history ||= []
|
|
85
|
+
@cycle_history << {
|
|
86
|
+
at: Time.now,
|
|
87
|
+
results_count: results.size,
|
|
88
|
+
generated: results.count { |r| r[:result][:success] }
|
|
89
|
+
}
|
|
90
|
+
@cycle_history.shift if @cycle_history.size > 50
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def last_cycle_at
|
|
95
|
+
mutex.synchronize { @last_cycle_at }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def cycle_count
|
|
99
|
+
mutex.synchronize { @cycle_count || 0 }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def total_generated
|
|
103
|
+
mutex.synchronize { @total_generated || 0 }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def mutex
|
|
107
|
+
@mutex ||= Mutex.new
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/legion/mcp/server.rb
CHANGED
|
@@ -47,6 +47,9 @@ require_relative 'tools/eval_results'
|
|
|
47
47
|
require_relative 'context_compiler'
|
|
48
48
|
require_relative 'embedding_index'
|
|
49
49
|
require_relative 'cold_start'
|
|
50
|
+
require_relative 'gap_detector'
|
|
51
|
+
require_relative 'function_generator'
|
|
52
|
+
require_relative 'self_generate'
|
|
50
53
|
require_relative 'tools/do_action'
|
|
51
54
|
require_relative 'tools/plan_action'
|
|
52
55
|
require_relative 'tools/discover_tools'
|
data/lib/legion/mcp/version.rb
CHANGED
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.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -128,6 +128,7 @@ files:
|
|
|
128
128
|
- lib/legion/mcp/context_compiler.rb
|
|
129
129
|
- lib/legion/mcp/context_guard.rb
|
|
130
130
|
- lib/legion/mcp/embedding_index.rb
|
|
131
|
+
- lib/legion/mcp/function_generator.rb
|
|
131
132
|
- lib/legion/mcp/gap_detector.rb
|
|
132
133
|
- lib/legion/mcp/observer.rb
|
|
133
134
|
- lib/legion/mcp/override_broadcast.rb
|
|
@@ -138,6 +139,7 @@ files:
|
|
|
138
139
|
- lib/legion/mcp/pattern_store.rb
|
|
139
140
|
- lib/legion/mcp/resources/extension_info.rb
|
|
140
141
|
- lib/legion/mcp/resources/runner_catalog.rb
|
|
142
|
+
- lib/legion/mcp/self_generate.rb
|
|
141
143
|
- lib/legion/mcp/server.rb
|
|
142
144
|
- lib/legion/mcp/settings.rb
|
|
143
145
|
- lib/legion/mcp/tier_router.rb
|