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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 918ab35b2f575f24b157f7f4aaf418e13a1f950b58bf387f17f921bd121f8b85
4
- data.tar.gz: d1de3e68b031d1c7a169db411d188e54ebd0107a191aec584981a95c852746f2
3
+ metadata.gz: 38c734e99f13544dcd04f25b33dbbaf996202518518ac5a900788d7d47a6f2ba
4
+ data.tar.gz: be38e1cba9d680076813e67ea08ca9dcdf264f146c809daa9345e302c3c6000d
5
5
  SHA512:
6
- metadata.gz: 42f453718ebe7af183b93b86724f40ec31927fed7f562f17c7abe9732d7732df02f35108d498640a7f330425f5a24d4b3602e4f28cef9166af0d5751a3f49c1e
7
- data.tar.gz: ccfce5e16a125063d3e4b74dff0b03fd4a6f0eab6e80b5480539c5de6adc9e61b8466acff2762a2501456a6ada0cb7743f7d0fe6c930ef50786438e5cba93a60
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
- FREQUENCY_THRESHOLD = 5
9
- CHAIN_THRESHOLD = 3
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 analyze
15
+ def detect_gaps
14
16
  gaps = []
15
- gaps.concat(detect_frequent_intents)
16
- gaps.concat(detect_repeated_chains)
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 detect_frequent_intents
21
- intents = Observer.recent_intents(Observer::INTENT_BUFFER_MAX)
22
- grouped = intents.group_by { |i| i[:matched_tool] }
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 |tool, occurrences|
25
- next if occurrences.size < FREQUENCY_THRESHOLD
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
- { type: :frequent_intent, tool: tool, count: occurrences.size,
29
- sample_intents: occurrences.last(3).map { |o| o[:intent] } }
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 detect_repeated_chains
34
- recent = Observer.recent(Observer::RING_BUFFER_MAX)
35
- chains = {}
36
- recent.each_cons(2) do |a, b|
37
- key = "#{a[:tool_name]}->#{b[:tool_name]}"
38
- chains[key] = (chains[key] || 0) + 1
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
- chains.filter_map do |chain, count|
42
- next if count < CHAIN_THRESHOLD
76
+ candidates.filter_map do |intent_hash, entry|
77
+ next if entry[:count] < 2
43
78
 
44
- { type: :repeated_chain, chain: chain.split('->'), count: count }
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 reset!
49
- # No persistent state to clear — analysis reads from Observer
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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.5.3'
5
+ VERSION = '0.5.5'
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.5.3
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