legion-mcp 0.7.4 → 0.8.0

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: 6a3e25d857f68078fd0cdee66c754af9f90d463b51e781c77fc98101a4f6417e
4
+ data.tar.gz: 210832fec6116565553d64f5903f9c6c2599b4f98d3aaa30a43d78ab6488322b
5
5
  SHA512:
6
- metadata.gz: cd0bf2a74ac997e5ce7db2bee56a6e9d821f6244ba211c41be054700bfeb7037c557079b6616403a4f188193133d2b2087f496a0ea7be43eec96694e6feed9b9
7
- data.tar.gz: af2d74fa1eeb888753a815af65e9a4527fc00d44dfdf4941abc9955f1da539606f3284413518b0b618bcd50c71894ef5f37750154cbf3f35ae224390a89b4204
6
+ metadata.gz: 33a52a4ad2246bc5332cdd9d2fa63c8ba285945b43c56c79b5fb22088315a51f53828ff3ccd3507ad39cc8613459dfa272aaccda73362a364c91acf21d5200bf
7
+ data.tar.gz: 4ebbba0174ae96289f20001c0acdaa2d7b475ae671f783cf0297533ddee50bc54ed889f313aff1442c3a840ef2b56637e990f063d412db369048924897518969
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # legion-mcp Changelog
2
2
 
3
+ ## [0.8.0] - 2026-04-12
4
+
5
+ ### Added
6
+ - `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
7
+ - `Tools::SkillDescribe` (`legion.skill.describe`) — MCP tool to get full detail for a named skill (`namespace:name` or bare `name`)
8
+ - `Tools::SkillInvoke` (`legion.skill.invoke`) — MCP tool to invoke a skill by name with optional `conversation_id` context
9
+ - `Tools::SkillCancel` (`legion.skill.cancel`) — MCP tool to cancel an active skill for a given conversation
10
+ - All four skill tools registered in `MCP_SPECIFIC_TOOLS` (now 10 total)
11
+ - `require_relative 'tools/skills'` added to `tools_loader.rb`
12
+
3
13
  ## [0.7.4] - 2026-04-06
4
14
 
5
15
  ### Changed
@@ -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.0'
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.0
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