legion-mcp 0.5.3 → 0.5.5
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 +25 -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 +16 -1
- data/lib/legion/mcp/tools/mind_growth_approve.rb +49 -0
- data/lib/legion/mcp/tools/mind_growth_build_queue.rb +44 -0
- data/lib/legion/mcp/tools/mind_growth_cognitive_profile.rb +44 -0
- data/lib/legion/mcp/tools/mind_growth_health.rb +44 -0
- data/lib/legion/mcp/tools/mind_growth_propose.rb +56 -0
- data/lib/legion/mcp/tools/mind_growth_status.rb +44 -0
- data/lib/legion/mcp/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38c734e99f13544dcd04f25b33dbbaf996202518518ac5a900788d7d47a6f2ba
|
|
4
|
+
data.tar.gz: be38e1cba9d680076813e67ea08ca9dcdf264f146c809daa9345e302c3c6000d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 340cd8ca1944b59f12bcbd32eb9338a284398477cc18fff805100dd60dab8ca71c27fbd37b1359d6e3936773063919515f21c9cbeb62c00d4f9bfa3c500c93c5
|
|
7
|
+
data.tar.gz: 926cb453173fc145b569da97c6168483622005a2b112b3831025a58f013135f66fd3a4110f8bd47e8f83b0459fc7022b8638e00f1844d52b530f1ad863fe9b12
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# legion-mcp Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.5] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Mind Growth Phase 7.2: 6 MCP tools for lex-mind-growth integration
|
|
7
|
+
- `legion.mind_growth_status` — growth status including proposals and cognitive coverage
|
|
8
|
+
- `legion.mind_growth_propose` — propose a new cognitive extension concept
|
|
9
|
+
- `legion.mind_growth_approve` — evaluate and score a proposal for approval
|
|
10
|
+
- `legion.mind_growth_build_queue` — list approved proposals in the build queue
|
|
11
|
+
- `legion.mind_growth_cognitive_profile` — analyze cognitive architecture coverage against reference models
|
|
12
|
+
- `legion.mind_growth_health` — extension fitness scores, prune candidates, and improvement candidates
|
|
13
|
+
- All 6 tools registered in `TOOL_CLASSES`; total tool count raised from 50 to 56
|
|
14
|
+
- Specs for all 6 new tools (38 examples)
|
|
15
|
+
- Updated `server_spec.rb` tool count assertion from 50 to 56
|
|
16
|
+
|
|
17
|
+
## [0.5.4] - 2026-03-24
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- TBI Phase 5: `GapDetector` — detects unmatched intents, high-failure tools, and stale candidates from Observer/PatternStore data
|
|
21
|
+
- TBI Phase 5: `FunctionGenerator` — LLM-powered tool spec generation from detected gaps, with validation and pattern registration
|
|
22
|
+
- TBI Phase 5: `SelfGenerate` — orchestrates gap detection + function generation cycles with cooldown, history tracking, and status reporting
|
|
23
|
+
- 89 new specs across gap_detector, function_generator, and self_generate
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Rewrote `GapDetector` from frequency-based to gap-type-based detection (unmatched, failure, stale) with priority scoring
|
|
27
|
+
|
|
3
28
|
## [0.5.3] - 2026-03-23
|
|
4
29
|
|
|
5
30
|
### 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'
|
|
@@ -55,6 +58,12 @@ require_relative 'tools/list_peers'
|
|
|
55
58
|
require_relative 'tools/notify_peer'
|
|
56
59
|
require_relative 'tools/broadcast_peers'
|
|
57
60
|
require_relative 'tools/mesh_status'
|
|
61
|
+
require_relative 'tools/mind_growth_status'
|
|
62
|
+
require_relative 'tools/mind_growth_propose'
|
|
63
|
+
require_relative 'tools/mind_growth_approve'
|
|
64
|
+
require_relative 'tools/mind_growth_build_queue'
|
|
65
|
+
require_relative 'tools/mind_growth_cognitive_profile'
|
|
66
|
+
require_relative 'tools/mind_growth_health'
|
|
58
67
|
require_relative 'catalog_bridge'
|
|
59
68
|
require_relative 'resources/runner_catalog'
|
|
60
69
|
require_relative 'resources/extension_info'
|
|
@@ -112,7 +121,13 @@ module Legion
|
|
|
112
121
|
Tools::ListPeers,
|
|
113
122
|
Tools::NotifyPeer,
|
|
114
123
|
Tools::BroadcastPeers,
|
|
115
|
-
Tools::MeshStatus
|
|
124
|
+
Tools::MeshStatus,
|
|
125
|
+
Tools::MindGrowthStatus,
|
|
126
|
+
Tools::MindGrowthPropose,
|
|
127
|
+
Tools::MindGrowthApprove,
|
|
128
|
+
Tools::MindGrowthBuildQueue,
|
|
129
|
+
Tools::MindGrowthCognitiveProfile,
|
|
130
|
+
Tools::MindGrowthHealth
|
|
116
131
|
].freeze
|
|
117
132
|
|
|
118
133
|
class << self
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MindGrowthApprove < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.mind_growth_approve'
|
|
8
|
+
description 'Evaluate and score a mind growth proposal for approval.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
proposal_id: { type: 'string', description: 'ID of the proposal to evaluate' }
|
|
13
|
+
},
|
|
14
|
+
required: ['proposal_id']
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(proposal_id:)
|
|
19
|
+
return error_response('lex-mind-growth is not available') unless mind_growth_available?
|
|
20
|
+
|
|
21
|
+
result = mind_growth_client.evaluate_proposal(proposal_id: proposal_id)
|
|
22
|
+
text_response(result)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
Legion::Logging.warn("MindGrowthApprove#call failed: #{e.message}") if defined?(Legion::Logging)
|
|
25
|
+
error_response("Failed to evaluate proposal: #{e.message}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def mind_growth_available?
|
|
31
|
+
defined?(Legion::Extensions::MindGrowth::Client)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def mind_growth_client
|
|
35
|
+
@mind_growth_client ||= Legion::Extensions::MindGrowth::Client.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def text_response(data)
|
|
39
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def error_response(msg)
|
|
43
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MindGrowthBuildQueue < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.mind_growth_build_queue'
|
|
8
|
+
description 'List the current build queue of approved mind growth proposals.'
|
|
9
|
+
|
|
10
|
+
input_schema(properties: {})
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def call
|
|
14
|
+
return error_response('lex-mind-growth is not available') unless mind_growth_available?
|
|
15
|
+
|
|
16
|
+
result = mind_growth_client.list_proposals(status: :approved)
|
|
17
|
+
text_response(result)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Legion::Logging.warn("MindGrowthBuildQueue#call failed: #{e.message}") if defined?(Legion::Logging)
|
|
20
|
+
error_response("Failed to get build queue: #{e.message}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def mind_growth_available?
|
|
26
|
+
defined?(Legion::Extensions::MindGrowth::Client)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def mind_growth_client
|
|
30
|
+
@mind_growth_client ||= Legion::Extensions::MindGrowth::Client.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def text_response(data)
|
|
34
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error_response(msg)
|
|
38
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MindGrowthCognitiveProfile < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.mind_growth_cognitive_profile'
|
|
8
|
+
description 'Analyze the current cognitive architecture coverage against reference models.'
|
|
9
|
+
|
|
10
|
+
input_schema(properties: {})
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def call
|
|
14
|
+
return error_response('lex-mind-growth is not available') unless mind_growth_available?
|
|
15
|
+
|
|
16
|
+
result = mind_growth_client.cognitive_profile
|
|
17
|
+
text_response(result)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Legion::Logging.warn("MindGrowthCognitiveProfile#call failed: #{e.message}") if defined?(Legion::Logging)
|
|
20
|
+
error_response("Failed to get cognitive profile: #{e.message}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def mind_growth_available?
|
|
26
|
+
defined?(Legion::Extensions::MindGrowth::Client)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def mind_growth_client
|
|
30
|
+
@mind_growth_client ||= Legion::Extensions::MindGrowth::Client.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def text_response(data)
|
|
34
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error_response(msg)
|
|
38
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MindGrowthHealth < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.mind_growth_health'
|
|
8
|
+
description 'Get extension fitness scores, prune candidates, and improvement candidates.'
|
|
9
|
+
|
|
10
|
+
input_schema(properties: {})
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def call
|
|
14
|
+
return error_response('lex-mind-growth is not available') unless mind_growth_available?
|
|
15
|
+
|
|
16
|
+
result = mind_growth_client.validate_fitness(extensions: [])
|
|
17
|
+
text_response(result)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Legion::Logging.warn("MindGrowthHealth#call failed: #{e.message}") if defined?(Legion::Logging)
|
|
20
|
+
error_response("Failed to get mind growth health: #{e.message}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def mind_growth_available?
|
|
26
|
+
defined?(Legion::Extensions::MindGrowth::Client)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def mind_growth_client
|
|
30
|
+
@mind_growth_client ||= Legion::Extensions::MindGrowth::Client.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def text_response(data)
|
|
34
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error_response(msg)
|
|
38
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MindGrowthPropose < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.mind_growth_propose'
|
|
8
|
+
description 'Propose a new cognitive extension concept for the architecture.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
category: { type: 'string',
|
|
13
|
+
description: 'Cognitive category (cognition, perception, introspection, ' \
|
|
14
|
+
'safety, communication, memory, motivation, coordination)' },
|
|
15
|
+
description: { type: 'string', description: 'Description of the proposed extension' },
|
|
16
|
+
name: { type: 'string', description: 'Optional extension name' }
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def call(params = {})
|
|
22
|
+
return error_response('lex-mind-growth is not available') unless mind_growth_available?
|
|
23
|
+
|
|
24
|
+
result = mind_growth_client.propose_concept(
|
|
25
|
+
category: params[:category]&.to_sym,
|
|
26
|
+
description: params[:description],
|
|
27
|
+
name: params[:name]
|
|
28
|
+
)
|
|
29
|
+
text_response(result)
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
Legion::Logging.warn("MindGrowthPropose#call failed: #{e.message}") if defined?(Legion::Logging)
|
|
32
|
+
error_response("Failed to propose concept: #{e.message}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def mind_growth_available?
|
|
38
|
+
defined?(Legion::Extensions::MindGrowth::Client)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mind_growth_client
|
|
42
|
+
@mind_growth_client ||= Legion::Extensions::MindGrowth::Client.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def text_response(data)
|
|
46
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def error_response(msg)
|
|
50
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MindGrowthStatus < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.mind_growth_status'
|
|
8
|
+
description 'Get current mind growth status including proposals and cognitive coverage.'
|
|
9
|
+
|
|
10
|
+
input_schema(properties: {})
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def call
|
|
14
|
+
return error_response('lex-mind-growth is not available') unless mind_growth_available?
|
|
15
|
+
|
|
16
|
+
result = mind_growth_client.growth_status
|
|
17
|
+
text_response(result)
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Legion::Logging.warn("MindGrowthStatus#call failed: #{e.message}") if defined?(Legion::Logging)
|
|
20
|
+
error_response("Failed to get mind growth status: #{e.message}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def mind_growth_available?
|
|
26
|
+
defined?(Legion::Extensions::MindGrowth::Client)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def mind_growth_client
|
|
30
|
+
@mind_growth_client ||= Legion::Extensions::MindGrowth::Client.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def text_response(data)
|
|
34
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error_response(msg)
|
|
38
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
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.5
|
|
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
|
|
@@ -175,6 +177,12 @@ files:
|
|
|
175
177
|
- lib/legion/mcp/tools/list_tasks.rb
|
|
176
178
|
- lib/legion/mcp/tools/list_workers.rb
|
|
177
179
|
- lib/legion/mcp/tools/mesh_status.rb
|
|
180
|
+
- lib/legion/mcp/tools/mind_growth_approve.rb
|
|
181
|
+
- lib/legion/mcp/tools/mind_growth_build_queue.rb
|
|
182
|
+
- lib/legion/mcp/tools/mind_growth_cognitive_profile.rb
|
|
183
|
+
- lib/legion/mcp/tools/mind_growth_health.rb
|
|
184
|
+
- lib/legion/mcp/tools/mind_growth_propose.rb
|
|
185
|
+
- lib/legion/mcp/tools/mind_growth_status.rb
|
|
178
186
|
- lib/legion/mcp/tools/notify_peer.rb
|
|
179
187
|
- lib/legion/mcp/tools/plan_action.rb
|
|
180
188
|
- lib/legion/mcp/tools/prompt_list.rb
|