legion-mcp 0.7.4 → 0.8.1

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: 1f74d80bf3fc37b0b0130f77f38d7b1611c3efa6ab193085022d3aa1462e3ad4
4
- data.tar.gz: a9ca6d64d2f3c089a787258e4ac88855d9f5d149c22eef1863bfa429157b70ed
3
+ metadata.gz: dbc548bee22e23b6829490dcb7df334ae19c470f4f0d5d9148831b125083e323
4
+ data.tar.gz: 56bc136cb3b5801dce2d7fed3ba947ff9de1abd64c3dd996caa67cdffca49cd1
5
5
  SHA512:
6
- metadata.gz: cd0bf2a74ac997e5ce7db2bee56a6e9d821f6244ba211c41be054700bfeb7037c557079b6616403a4f188193133d2b2087f496a0ea7be43eec96694e6feed9b9
7
- data.tar.gz: af2d74fa1eeb888753a815af65e9a4527fc00d44dfdf4941abc9955f1da539606f3284413518b0b618bcd50c71894ef5f37750154cbf3f35ae224390a89b4204
6
+ metadata.gz: 62db181395833185ab2e9b11b7ef3281a2e49761aab8d61feabce4fd19389fe768af86b26d0e24501ebb0ca2237ec87bd820ca18417b4eda3e77921fade69508
7
+ data.tar.gz: f5f1429b9cd74bcfaaf61083a38c5c5245644beee9e6ae3910ccb947f0f6809b8e9ef664bdce3d6a4ad3b9ec493788d92bb587c0d53bde7b75d7bed3b5a0fef0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # legion-mcp Changelog
2
2
 
3
+ ## [0.8.1] - 2026-04-14
4
+
5
+ ### Fixed
6
+ - `ContextCompiler::CATEGORIES` now includes a `:skills` category listing all four `legion.skill.*` tools (`legion.skill.list`, `legion.skill.describe`, `legion.skill.invoke`, `legion.skill.cancel`). Previously they were absent from `CATEGORIES` so `compressed_catalog` never surfaced them and `legion.do intent:"list all skills"` would misroute to an auto-discovered swarm-github runner, returning "missing keywords: :owner, :repo, :pull_number"
7
+ - `ContextCompiler#keyword_score_map` now adds a +3 bonus per intent keyword that matches tool name terms (split on `.`), preventing semantic-score drift from lifting generic runner stubs above correctly-named skill tools when embeddings are active
8
+
9
+ ## [0.8.0] - 2026-04-12
10
+
11
+ ### Added
12
+ - `Tools::SkillList` (`legion.skill.list`) — MCP tool to list all skills registered in the Legion daemon with name, namespace, description, trigger words, and trigger type
13
+ - `Tools::SkillDescribe` (`legion.skill.describe`) — MCP tool to get full detail for a named skill (`namespace:name` or bare `name`)
14
+ - `Tools::SkillInvoke` (`legion.skill.invoke`) — MCP tool to invoke a skill by name with optional `conversation_id` context
15
+ - `Tools::SkillCancel` (`legion.skill.cancel`) — MCP tool to cancel an active skill for a given conversation
16
+ - All four skill tools registered in `MCP_SPECIFIC_TOOLS` (now 10 total)
17
+ - `require_relative 'tools/skills'` added to `tools_loader.rb`
18
+
3
19
  ## [0.7.4] - 2026-04-06
4
20
 
5
21
  ### Changed
@@ -71,6 +71,10 @@ module Legion
71
71
  tools: %w[legion.eval_list legion.eval_run legion.eval_results],
72
72
  summary: 'Evaluation management - list evaluators, run evaluations, view results.'
73
73
  },
74
+ skills: {
75
+ tools: %w[legion.skill.list legion.skill.describe legion.skill.invoke legion.skill.cancel],
76
+ summary: 'Skill management - list, describe, invoke, and cancel LLM skills.'
77
+ },
74
78
  meta: {
75
79
  tools: %w[legion.do legion.tools legion.plan_action legion.structural_index],
76
80
  summary: 'Meta-tools - natural language routing, tool discovery, planning, structural index.'
@@ -214,6 +218,8 @@ module Legion
214
218
  tool_index.values.to_h do |entry|
215
219
  haystack = "#{entry[:name].downcase} #{entry[:description].downcase}"
216
220
  score = keywords.count { |kw| haystack.include?(kw) }
221
+ name_terms = entry[:name].downcase.tr('._-', ' ').split
222
+ score += (keywords & name_terms).length * 3
217
223
  [entry[:name], score]
218
224
  end
219
225
  end
@@ -31,7 +31,11 @@ module Legion
31
31
  Tools::StructuralIndexTool,
32
32
  Tools::ToolAudit,
33
33
  Tools::StateDiff,
34
- Tools::SearchSessions
34
+ Tools::SearchSessions,
35
+ Tools::SkillList,
36
+ Tools::SkillDescribe,
37
+ Tools::SkillInvoke,
38
+ Tools::SkillCancel
35
39
  ].freeze
36
40
 
37
41
  @tool_registry = Concurrent::Array.new(MCP_SPECIFIC_TOOLS)
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module MCP
7
+ module Tools
8
+ class SkillList < ::MCP::Tool
9
+ tool_name 'legion.skill.list'
10
+ description 'List all skills available in this Legion instance.'
11
+
12
+ input_schema(properties: {})
13
+
14
+ class << self
15
+ include Legion::Logging::Helper
16
+
17
+ def call
18
+ log.info('Starting legion.mcp.tools.skill_list.call')
19
+ return error_response('Skills not available: legion-llm not loaded') unless defined?(Legion::LLM::Skills::Registry)
20
+
21
+ skills = Legion::LLM::Skills::Registry.all.map do |s|
22
+ {
23
+ name: s.skill_name,
24
+ namespace: s.namespace,
25
+ description: s.description,
26
+ trigger_words: s.trigger_words,
27
+ trigger: s.trigger
28
+ }
29
+ end
30
+ text_response({ skills: skills, count: skills.size })
31
+ rescue StandardError => e
32
+ handle_exception(e, level: :warn, operation: 'legion.mcp.tools.skill_list.call')
33
+ log.warn("SkillList#call failed: #{e.message}")
34
+ error_response("Failed to list skills: #{e.message}")
35
+ end
36
+
37
+ private
38
+
39
+ def text_response(data)
40
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
41
+ end
42
+
43
+ def error_response(msg)
44
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
45
+ end
46
+ end
47
+ end
48
+
49
+ class SkillDescribe < ::MCP::Tool
50
+ tool_name 'legion.skill.describe'
51
+ description 'Describe a specific skill by its namespace:name key or bare name.'
52
+
53
+ input_schema(
54
+ properties: {
55
+ name: {
56
+ type: 'string',
57
+ description: 'Skill key in namespace:name format (e.g. "superpowers:brainstorming") or bare skill name'
58
+ }
59
+ },
60
+ required: ['name']
61
+ )
62
+
63
+ class << self
64
+ include Legion::Logging::Helper
65
+
66
+ def call(name:)
67
+ log.info('Starting legion.mcp.tools.skill_describe.call')
68
+ return error_response('Skills not available: legion-llm not loaded') unless defined?(Legion::LLM::Skills::Registry)
69
+
70
+ skill = find_skill(name)
71
+ return error_response("Skill '#{name}' not found") if skill.nil?
72
+
73
+ text_response({
74
+ name: skill.skill_name,
75
+ namespace: skill.namespace,
76
+ description: skill.description,
77
+ trigger_words: skill.trigger_words,
78
+ trigger: skill.trigger,
79
+ follows_skill: skill.follows_skill,
80
+ steps: skill.steps
81
+ })
82
+ rescue StandardError => e
83
+ handle_exception(e, level: :warn, operation: 'legion.mcp.tools.skill_describe.call')
84
+ log.warn("SkillDescribe#call failed: #{e.message}")
85
+ error_response("Failed to describe skill: #{e.message}")
86
+ end
87
+
88
+ private
89
+
90
+ def find_skill(name)
91
+ skill = Legion::LLM::Skills::Registry.find(name)
92
+ return skill unless skill.nil?
93
+ return nil if name.include?(':')
94
+
95
+ Legion::LLM::Skills::Registry.all.find { |s| s.skill_name == name }
96
+ end
97
+
98
+ def text_response(data)
99
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
100
+ end
101
+
102
+ def error_response(msg)
103
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
104
+ end
105
+ end
106
+ end
107
+
108
+ class SkillInvoke < ::MCP::Tool
109
+ tool_name 'legion.skill.invoke'
110
+ description 'Invoke a skill for a conversation. The skill will run its configured steps.'
111
+
112
+ input_schema(
113
+ properties: {
114
+ name: {
115
+ type: 'string',
116
+ description: 'Skill key in namespace:name format (e.g. "superpowers:brainstorming")'
117
+ },
118
+ conversation_id: {
119
+ type: 'string',
120
+ description: 'Conversation ID to associate the skill run with (optional — generated if omitted)'
121
+ },
122
+ initial_message: {
123
+ type: 'string',
124
+ description: 'Optional initial message to seed the skill run (defaults to "start skill")'
125
+ }
126
+ },
127
+ required: ['name']
128
+ )
129
+
130
+ class << self
131
+ include Legion::Logging::Helper
132
+
133
+ def call(name:, conversation_id: nil, initial_message: nil)
134
+ log.info('Starting legion.mcp.tools.skill_invoke.call')
135
+ return error_response('Skills not available: legion-llm not loaded') unless defined?(Legion::LLM::Skills::Registry)
136
+
137
+ skill = Legion::LLM::Skills::Registry.find(name)
138
+ return error_response("Skill '#{name}' not found") if skill.nil?
139
+
140
+ conv_id = conversation_id || "conv_#{::SecureRandom.hex(8)}"
141
+ invoke_skill(name, conv_id, initial_message)
142
+ rescue StandardError => e
143
+ handle_exception(e, level: :warn, operation: 'legion.mcp.tools.skill_invoke.call')
144
+ log.warn("SkillInvoke#call failed: #{e.message}")
145
+ error_response("Failed to invoke skill: #{e.message}")
146
+ end
147
+
148
+ private
149
+
150
+ def invoke_skill(name, conv_id, initial_message)
151
+ return error_response('ConversationStore not available — cannot invoke skill') unless defined?(Legion::LLM::ConversationStore)
152
+
153
+ unless defined?(Legion::LLM::Pipeline::Executor) && defined?(Legion::LLM::Pipeline::Request)
154
+ Legion::LLM::ConversationStore.set_skill_state(conv_id, skill_key: name, resume_at: 0)
155
+ return text_response({ invoked: false, skill: name, conversation_id: conv_id,
156
+ note: 'skill state queued — pipeline executor not available' })
157
+ end
158
+
159
+ Legion::LLM::ConversationStore.set_skill_state(conv_id, skill_key: name, resume_at: 0)
160
+ req = Legion::LLM::Pipeline::Request.build(
161
+ messages: [{ role: :user, content: initial_message || 'start skill' }],
162
+ conversation_id: conv_id,
163
+ metadata: { skill_invoke: true },
164
+ stream: false
165
+ )
166
+ result = Legion::LLM::Pipeline::Executor.new(req).call
167
+ text_response({ invoked: true, skill: name, conversation_id: conv_id,
168
+ content: result.message[:content] })
169
+ rescue StandardError => e
170
+ Legion::LLM::ConversationStore.clear_skill_state(conv_id) if defined?(Legion::LLM::ConversationStore)
171
+ raise e
172
+ end
173
+
174
+ def text_response(data)
175
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
176
+ end
177
+
178
+ def error_response(msg)
179
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
180
+ end
181
+ end
182
+ end
183
+
184
+ class SkillCancel < ::MCP::Tool
185
+ tool_name 'legion.skill.cancel'
186
+ description 'Cancel an active skill run for a conversation.'
187
+
188
+ input_schema(
189
+ properties: {
190
+ conversation_id: {
191
+ type: 'string',
192
+ description: 'Conversation ID whose active skill should be cancelled'
193
+ }
194
+ },
195
+ required: ['conversation_id']
196
+ )
197
+
198
+ class << self
199
+ include Legion::Logging::Helper
200
+
201
+ def call(conversation_id:)
202
+ log.info('Starting legion.mcp.tools.skill_cancel.call')
203
+ return error_response('ConversationStore not available') unless defined?(Legion::LLM::ConversationStore)
204
+
205
+ result = Legion::LLM::ConversationStore.cancel_skill!(conversation_id)
206
+ if result
207
+ text_response({ cancelled: true, skill_key: result[:skill_key] })
208
+ else
209
+ text_response({ cancelled: false, reason: 'not_running' })
210
+ end
211
+ rescue StandardError => e
212
+ handle_exception(e, level: :warn, operation: 'legion.mcp.tools.skill_cancel.call')
213
+ log.warn("SkillCancel#call failed: #{e.message}")
214
+ error_response("Failed to cancel skill: #{e.message}")
215
+ end
216
+
217
+ private
218
+
219
+ def text_response(data)
220
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
221
+ end
222
+
223
+ def error_response(msg)
224
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -69,3 +69,4 @@ require_relative 'tools/structural_index'
69
69
  require_relative 'tools/tool_audit'
70
70
  require_relative 'tools/state_diff'
71
71
  require_relative 'tools/search_sessions'
72
+ require_relative 'tools/skills'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.7.4'
5
+ VERSION = '0.8.1'
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.7.4
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -209,6 +209,7 @@ files:
209
209
  - lib/legion/mcp/tools/run_task.rb
210
210
  - lib/legion/mcp/tools/search_sessions.rb
211
211
  - lib/legion/mcp/tools/show_worker.rb
212
+ - lib/legion/mcp/tools/skills.rb
212
213
  - lib/legion/mcp/tools/state_diff.rb
213
214
  - lib/legion/mcp/tools/structural_index.rb
214
215
  - lib/legion/mcp/tools/team_summary.rb