legion-mcp 0.7.2 → 0.7.3

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: cd343295ee4f59ac0614a3058aabfac15045eb01dd0b5e9dc5506f420059d577
4
- data.tar.gz: 0cb3173967bcdb22b7c2d230f173c26d4884dc05b2acbbb5d000041c2509e4aa
3
+ metadata.gz: 60a58443e45b7bb2ffc897534a7b092bf4d5c152b2206e4268a8a8c9ff6a1755
4
+ data.tar.gz: 178682013f6738d801d382b32e85161044c2ed48a0e8cc979a0361f542fa0f70
5
5
  SHA512:
6
- metadata.gz: 56c5321aa074674433bf222fd3e513217b983e11712dad1d1fff24bab90e0de9acf0896c3d54f38a67f585ef666ffb0d5e538de787e28e04f9f8e00f59ba3787
7
- data.tar.gz: 4950bfc2b5a43bb7ef7dabb34d3b93258611b9d85a551910793dac1229f463b464ea0c44cfc690bb7a729b44493fdb4b1fe0fe9801f0f0e4cc9cbe657b2dfc2d
6
+ metadata.gz: 0b18864b7e771313db5d679018614dfd3b380fa2d55eb4f954310f90881efe04bc49c082e090db7e6b2b0b405c48a9b91d62f35ffb6a268ff64b2a9652c8f1db
7
+ data.tar.gz: 26b516e3471032d36cbcc580fed82da09615be5dd0777321ed3147d020afe2260dab6175ab086a4e3cfa60471160b481a2de99c9d55e328e0925bf534e2a6b36
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ pkg/
4
4
  *.gem
5
5
  .bundle/
6
6
  vendor/
7
+ .worktrees
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### Added
6
+ - `Legion::MCP::ToolAdapter` - wraps Tools::Base into MCP::Tool
7
+ - `rebuild_tool_registry` method for dynamic tool registration
8
+
9
+ ### Changed
10
+ - Tool registry populated from `Legion::Tools::Registry` via ToolAdapter
11
+ - `DeferredRegistry` reads canonical classification with `reset_cache!`
12
+ - `EmbeddingIndex` uses `Tools::EmbeddingCache` for persistent caching
13
+ - `FunctionDiscovery` delegates to `Tools::Discovery` with double-fire guard
14
+
15
+ ### Removed
16
+ - Direct tool ownership (tools live in LegionIO now)
17
+
5
18
  ## [0.7.2] - 2026-04-03
6
19
 
7
20
  ### Fixed
@@ -23,14 +23,25 @@ module Legion
23
23
 
24
24
  module_function
25
25
 
26
+ def reset_cache!
27
+ @always_loaded_cache = nil
28
+ end
29
+
26
30
  def enabled?
27
31
  setting = Legion::Settings.dig(:mcp, :deferred_loading, :enabled)
28
32
  setting.nil? || setting
29
33
  end
30
34
 
31
35
  def always_loaded_tools
36
+ return @always_loaded_cache if @always_loaded_cache
37
+
38
+ base = ALWAYS_LOADED.dup
39
+ if defined?(Legion::Tools::Registry) && Legion::Tools::Registry.respond_to?(:tools)
40
+ registry_always = Legion::Tools::Registry.tools(:always)
41
+ base |= registry_always.map(&:tool_name) if registry_always.is_a?(Array)
42
+ end
32
43
  custom = Legion::Settings.dig(:mcp, :deferred_loading, :always_loaded)
33
- custom.is_a?(Array) ? (ALWAYS_LOADED | custom) : ALWAYS_LOADED
44
+ @always_loaded_cache = custom.is_a?(Array) ? (base | custom) : base
34
45
  end
35
46
 
36
47
  def deferred?(tool_class)
@@ -10,9 +10,25 @@ module Legion
10
10
  def build_from_tool_data(tool_data, embedder: default_embedder)
11
11
  @embedder = embedder
12
12
  mutex.synchronize do
13
- tool_data.each do |tool|
14
- composite = build_composite(tool[:name], tool[:description], tool[:params])
13
+ composites = tool_data.to_h do |tool|
14
+ [tool[:name], build_composite(tool[:name], tool[:description], tool[:params])]
15
+ end
16
+
17
+ cached_vectors = bulk_cache_lookup(composites.values)
18
+
19
+ uncached_names = composites.keys.reject { |name| cached_vectors.key?(composites[name]) }
20
+ newly_embedded = {}
21
+ uncached_names.each do |name|
22
+ composite = composites[name]
15
23
  vector = safe_embed(composite, embedder)
24
+ newly_embedded[composite] = vector if vector
25
+ end
26
+
27
+ bulk_cache_store(newly_embedded) unless newly_embedded.empty?
28
+
29
+ tool_data.each do |tool|
30
+ composite = composites[tool[:name]]
31
+ vector = cached_vectors[composite] || newly_embedded[composite]
16
32
  next unless vector
17
33
 
18
34
  index[tool[:name]] = {
@@ -86,6 +102,26 @@ module Legion
86
102
  @mutex ||= Mutex.new
87
103
  end
88
104
 
105
+ def bulk_cache_lookup(composite_texts)
106
+ return {} unless defined?(Legion::Tools::EmbeddingCache) &&
107
+ Legion::Tools::EmbeddingCache.respond_to?(:bulk_lookup)
108
+
109
+ Legion::Tools::EmbeddingCache.bulk_lookup(composite_texts)
110
+ rescue StandardError => e
111
+ handle_exception(e, level: :debug, operation: 'legion.mcp.embedding_index.bulk_cache_lookup')
112
+ {}
113
+ end
114
+
115
+ def bulk_cache_store(composite_to_vector)
116
+ return unless defined?(Legion::Tools::EmbeddingCache) &&
117
+ Legion::Tools::EmbeddingCache.respond_to?(:bulk_store)
118
+
119
+ Legion::Tools::EmbeddingCache.bulk_store(composite_to_vector)
120
+ rescue StandardError => e
121
+ handle_exception(e, level: :debug, operation: 'legion.mcp.embedding_index.bulk_cache_store')
122
+ nil
123
+ end
124
+
89
125
  def build_composite(name, description, params)
90
126
  parts = [name, '--', description]
91
127
  parts << "Params: #{params.join(', ')}" unless params.empty?
@@ -7,7 +7,16 @@ module Legion
7
7
 
8
8
  module_function
9
9
 
10
- def discover_and_register
10
+ def discover_and_register # rubocop:disable Metrics/PerceivedComplexity
11
+ return if @discovery_fired
12
+
13
+ @discovery_fired = true
14
+
15
+ if defined?(Legion::Tools::Discovery) && Legion::Tools::Discovery.respond_to?(:discover_and_register)
16
+ Legion::Tools::Discovery.discover_and_register
17
+ return
18
+ end
19
+
11
20
  return unless defined?(Legion::Extensions)
12
21
 
13
22
  extensions =
@@ -26,6 +35,10 @@ module Legion
26
35
  end
27
36
  end
28
37
 
38
+ def reset_discovery!
39
+ @discovery_fired = false
40
+ end
41
+
29
42
  def build_tools_from_runner(runner_module)
30
43
  return unless runner_module.respond_to?(:settings) && runner_module.settings.is_a?(Hash)
31
44
 
@@ -3,76 +3,14 @@
3
3
  require_relative 'observer'
4
4
  require_relative 'logging_support'
5
5
  require_relative 'usage_filter'
6
- require_relative 'tools/run_task'
7
- require_relative 'tools/describe_runner'
8
- require_relative 'tools/list_tasks'
9
- require_relative 'tools/get_task'
10
- require_relative 'tools/delete_task'
11
- require_relative 'tools/get_task_logs'
12
- require_relative 'tools/list_chains'
13
- require_relative 'tools/create_chain'
14
- require_relative 'tools/update_chain'
15
- require_relative 'tools/delete_chain'
16
- require_relative 'tools/list_relationships'
17
- require_relative 'tools/create_relationship'
18
- require_relative 'tools/update_relationship'
19
- require_relative 'tools/delete_relationship'
20
- require_relative 'tools/list_extensions'
21
- require_relative 'tools/get_extension'
22
- require_relative 'tools/enable_extension'
23
- require_relative 'tools/disable_extension'
24
- require_relative 'tools/list_schedules'
25
- require_relative 'tools/create_schedule'
26
- require_relative 'tools/update_schedule'
27
- require_relative 'tools/delete_schedule'
28
- require_relative 'tools/get_status'
29
- require_relative 'tools/get_config'
30
- require_relative 'tools/list_workers'
31
- require_relative 'tools/show_worker'
32
- require_relative 'tools/worker_lifecycle'
33
- require_relative 'tools/worker_costs'
34
- require_relative 'tools/team_summary'
35
- require_relative 'tools/routing_stats'
36
- require_relative 'tools/rbac_check'
37
- require_relative 'tools/rbac_assignments'
38
- require_relative 'tools/rbac_grants'
39
- require_relative 'tools/prompt_list'
40
- require_relative 'tools/prompt_show'
41
- require_relative 'tools/prompt_run'
42
- require_relative 'tools/dataset_list'
43
- require_relative 'tools/dataset_show'
44
- require_relative 'tools/experiment_results'
45
- require_relative 'tools/eval_list'
46
- require_relative 'tools/eval_run'
47
- require_relative 'tools/eval_results'
6
+ require_relative 'tools_loader'
7
+ require_relative 'tool_adapter'
48
8
  require_relative 'context_compiler'
49
9
  require_relative 'embedding_index'
50
10
  require_relative 'cold_start'
51
11
  require_relative 'gap_detector'
52
12
  require_relative 'function_discovery'
53
13
  require_relative 'self_generate'
54
- require_relative 'tools/do_action'
55
- require_relative 'tools/plan_action'
56
- require_relative 'tools/discover_tools'
57
- require_relative 'tools/ask_peer'
58
- require_relative 'tools/list_peers'
59
- require_relative 'tools/notify_peer'
60
- require_relative 'tools/broadcast_peers'
61
- require_relative 'tools/mesh_status'
62
- require_relative 'tools/mind_growth_status'
63
- require_relative 'tools/mind_growth_propose'
64
- require_relative 'tools/mind_growth_approve'
65
- require_relative 'tools/mind_growth_build_queue'
66
- require_relative 'tools/mind_growth_cognitive_profile'
67
- require_relative 'tools/mind_growth_health'
68
- require_relative 'tools/query_knowledge'
69
- require_relative 'tools/knowledge_health'
70
- require_relative 'tools/knowledge_context'
71
- require_relative 'tools/absorb'
72
- require_relative 'tools/structural_index'
73
- require_relative 'tools/tool_audit'
74
- require_relative 'tools/state_diff'
75
- require_relative 'tools/search_sessions'
76
14
  require_relative 'structural_index'
77
15
  require_relative 'state_tracker'
78
16
  require_relative 'tool_quality'
@@ -85,7 +23,8 @@ require_relative 'resources/extension_info'
85
23
 
86
24
  module Legion
87
25
  module MCP
88
- module Server
26
+ module Server # rubocop:disable Metrics/ModuleLength
27
+ # All built-in tool classes loaded via tools_loader.rb
89
28
  STATIC_TOOLS = [
90
29
  Tools::RunTask,
91
30
  Tools::DescribeRunner,
@@ -157,10 +96,26 @@ module Legion
157
96
  @tool_registry_lock = Mutex.new
158
97
 
159
98
  class << self # rubocop:disable Metrics/ClassLength
160
- include CatalogBridge
161
-
162
99
  attr_reader :tool_registry
163
100
 
101
+ def rebuild_tool_registry
102
+ @tool_registry_lock.synchronize do
103
+ @tool_registry = Concurrent::Array.new(STATIC_TOOLS)
104
+
105
+ if defined?(Legion::Tools::Registry) && Legion::Tools::Registry.respond_to?(:all_tools)
106
+ Legion::Tools::Registry.all_tools.each do |legion_tool_class|
107
+ next if @tool_registry.any? { |tc| tc.tool_name == legion_tool_class.tool_name }
108
+
109
+ adapted = ToolAdapter.from_legion_tool(legion_tool_class)
110
+ @tool_registry << adapted
111
+ end
112
+ end
113
+
114
+ DeferredRegistry.reset_cache! if defined?(DeferredRegistry) && DeferredRegistry.respond_to?(:reset_cache!)
115
+ reset_caches!
116
+ end
117
+ end
118
+
164
119
  def register_tool(tool_class)
165
120
  @tool_registry_lock.synchronize do
166
121
  return if tool_registry.any? { |tc| tc.tool_name == tool_class.tool_name }
@@ -192,7 +147,10 @@ module Legion
192
147
  EmbeddingIndex.reset! if defined?(EmbeddingIndex) && EmbeddingIndex.respond_to?(:reset!)
193
148
  end
194
149
 
195
- def build(identity: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
150
+ def build(identity: nil) # rubocop:disable Metrics/MethodLength
151
+ rebuild_tool_registry
152
+ register_catalog_listener
153
+
196
154
  LoggingSupport.info(
197
155
  'server.build.start',
198
156
  identity: LoggingSupport.summarize_identity(identity),
@@ -224,14 +182,12 @@ module Legion
224
182
 
225
183
  PatternStore.hydrate_from_l2 if defined?(PatternStore)
226
184
  ColdStart.load_community_patterns if defined?(ColdStart)
227
- FunctionDiscovery.discover_and_register if defined?(Legion::Extensions)
228
- register_catalog_tools
185
+ run_function_discovery
229
186
  populate_embedding_index
230
187
 
231
188
  Resources::RunnerCatalog.register(server)
232
189
  Resources::ExtensionInfo.register_read_handler(server)
233
190
 
234
- register_catalog_listener
235
191
  hydrate_override_confidence
236
192
 
237
193
  LoggingSupport.info(
@@ -307,8 +263,63 @@ module Legion
307
263
  end
308
264
  end
309
265
 
266
+ def dynamic_tool_list
267
+ static = tool_registry.map do |klass|
268
+ { name: klass.tool_name, description: klass.description,
269
+ input_schema: klass.input_schema, source: :builtin, klass: klass }
270
+ end
271
+
272
+ dynamic = if defined?(Legion::Extensions::Catalog::Registry)
273
+ Legion::Extensions::Catalog::Registry.for_mcp.map(&:to_mcp_tool)
274
+ else
275
+ []
276
+ end
277
+
278
+ static + dynamic
279
+ end
280
+
281
+ def dispatch_catalog_tool(tool_name, arguments)
282
+ return nil unless defined?(Legion::Extensions::Catalog::Registry)
283
+
284
+ cap = Legion::Extensions::Catalog::Registry.find_by_mcp_name(tool_name)
285
+ return nil unless cap
286
+
287
+ segments = cap.extension.delete_prefix('lex-').split('-')
288
+ runner_path = (%w[Legion Extensions] + segments.map(&:capitalize) + ['Runners', cap.runner]).join('::')
289
+ runner = Kernel.const_get(runner_path)
290
+ fn = cap.function.to_sym
291
+ result = runner.send(fn, **(arguments || {}).transform_keys(&:to_sym))
292
+ { status: :success, result: result, source: :catalog }
293
+ rescue NameError => e
294
+ handle_exception(e, level: :warn, operation: 'legion.mcp.server.dispatch_catalog_tool')
295
+ nil
296
+ rescue StandardError => e
297
+ handle_exception(e, level: :error, operation: 'legion.mcp.server.dispatch_catalog_tool')
298
+ { status: :error, error: e.message, source: :catalog }
299
+ end
300
+
301
+ def register_catalog_listener
302
+ return unless defined?(Legion::Extensions::Catalog::Registry)
303
+ return unless Legion::Extensions::Catalog::Registry.respond_to?(:on_change)
304
+
305
+ Legion::Extensions::Catalog::Registry.on_change { Legion::MCP.reset! }
306
+ end
307
+
310
308
  private
311
309
 
310
+ def run_function_discovery
311
+ FunctionDiscovery.reset_discovery!
312
+ FunctionDiscovery.discover_and_register
313
+ end
314
+
315
+ def hydrate_override_confidence
316
+ return unless defined?(Legion::LLM::OverrideConfidence)
317
+ return unless Legion::LLM::OverrideConfidence.respond_to?(:hydrate_from_l2)
318
+
319
+ Legion::LLM::OverrideConfidence.hydrate_from_l2
320
+ Legion::LLM::OverrideConfidence.hydrate_from_apollo if Legion::LLM::OverrideConfidence.respond_to?(:hydrate_from_apollo)
321
+ end
322
+
312
323
  def install_deferred_tools_list_handler(server)
313
324
  handlers = server.instance_variable_get(:@handlers)
314
325
  return unless handlers
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ class ToolAdapter < ::MCP::Tool
6
+ class << self
7
+ def from_legion_tool(tool_class)
8
+ Class.new(::MCP::Tool) do
9
+ tool_name tool_class.tool_name
10
+ description tool_class.description
11
+ input_schema(tool_class.input_schema || { properties: {} })
12
+
13
+ define_singleton_method(:legion_tool_class) { tool_class }
14
+
15
+ define_singleton_method(:call) do |**params|
16
+ result = tool_class.call(**params)
17
+ if result.is_a?(Hash) && result[:content]
18
+ content = result[:content].map do |c|
19
+ { type: c[:type] || 'text', text: c[:text] || '' }
20
+ end
21
+ ::MCP::Tool::Response.new(content, error: result[:error] || false)
22
+ else
23
+ text = result.is_a?(String) ? result : Legion::JSON.dump(result)
24
+ error = result.is_a?(Hash) ? !!result[:error] : false
25
+ ::MCP::Tool::Response.new([{ type: 'text', text: text }], error: error)
26
+ end
27
+ rescue StandardError => e
28
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: e.message }) }], error: true)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requires all built-in MCP tool files so they are defined as Ruby constants.
4
+ # server.rb no longer owns these requires — Legion::Tools::Registry provides
5
+ # runtime registration, but the classes must still be defined for specs and
6
+ # for any code that references Legion::MCP::Tools::* directly.
7
+
8
+ require_relative 'tools/run_task'
9
+ require_relative 'tools/describe_runner'
10
+ require_relative 'tools/list_tasks'
11
+ require_relative 'tools/get_task'
12
+ require_relative 'tools/delete_task'
13
+ require_relative 'tools/get_task_logs'
14
+ require_relative 'tools/list_chains'
15
+ require_relative 'tools/create_chain'
16
+ require_relative 'tools/update_chain'
17
+ require_relative 'tools/delete_chain'
18
+ require_relative 'tools/list_relationships'
19
+ require_relative 'tools/create_relationship'
20
+ require_relative 'tools/update_relationship'
21
+ require_relative 'tools/delete_relationship'
22
+ require_relative 'tools/list_extensions'
23
+ require_relative 'tools/get_extension'
24
+ require_relative 'tools/enable_extension'
25
+ require_relative 'tools/disable_extension'
26
+ require_relative 'tools/list_schedules'
27
+ require_relative 'tools/create_schedule'
28
+ require_relative 'tools/update_schedule'
29
+ require_relative 'tools/delete_schedule'
30
+ require_relative 'tools/get_status'
31
+ require_relative 'tools/get_config'
32
+ require_relative 'tools/list_workers'
33
+ require_relative 'tools/show_worker'
34
+ require_relative 'tools/worker_lifecycle'
35
+ require_relative 'tools/worker_costs'
36
+ require_relative 'tools/team_summary'
37
+ require_relative 'tools/routing_stats'
38
+ require_relative 'tools/rbac_check'
39
+ require_relative 'tools/rbac_assignments'
40
+ require_relative 'tools/rbac_grants'
41
+ require_relative 'tools/prompt_list'
42
+ require_relative 'tools/prompt_show'
43
+ require_relative 'tools/prompt_run'
44
+ require_relative 'tools/dataset_list'
45
+ require_relative 'tools/dataset_show'
46
+ require_relative 'tools/experiment_results'
47
+ require_relative 'tools/eval_list'
48
+ require_relative 'tools/eval_run'
49
+ require_relative 'tools/eval_results'
50
+ require_relative 'tools/do_action'
51
+ require_relative 'tools/plan_action'
52
+ require_relative 'tools/discover_tools'
53
+ require_relative 'tools/ask_peer'
54
+ require_relative 'tools/list_peers'
55
+ require_relative 'tools/notify_peer'
56
+ require_relative 'tools/broadcast_peers'
57
+ require_relative 'tools/mesh_status'
58
+ require_relative 'tools/mind_growth_status'
59
+ require_relative 'tools/mind_growth_propose'
60
+ require_relative 'tools/mind_growth_approve'
61
+ require_relative 'tools/mind_growth_build_queue'
62
+ require_relative 'tools/mind_growth_cognitive_profile'
63
+ require_relative 'tools/mind_growth_health'
64
+ require_relative 'tools/query_knowledge'
65
+ require_relative 'tools/knowledge_health'
66
+ require_relative 'tools/knowledge_context'
67
+ require_relative 'tools/absorb'
68
+ require_relative 'tools/structural_index'
69
+ require_relative 'tools/tool_audit'
70
+ require_relative 'tools/state_diff'
71
+ require_relative 'tools/search_sessions'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.7.2'
5
+ VERSION = '0.7.3'
6
6
  end
7
7
  end
data/lib/legion/mcp.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'mcp/version'
9
9
  require_relative 'mcp/settings'
10
10
  require_relative 'mcp/auth'
11
11
  require_relative 'mcp/tool_governance'
12
+ require_relative 'mcp/tools_loader'
12
13
  require_relative 'mcp/server'
13
14
  require_relative 'mcp/override_broadcast'
14
15
  require_relative 'mcp/client'
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.2
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -152,6 +152,7 @@ files:
152
152
  - lib/legion/mcp/state_tracker.rb
153
153
  - lib/legion/mcp/structural_index.rb
154
154
  - lib/legion/mcp/tier_router.rb
155
+ - lib/legion/mcp/tool_adapter.rb
155
156
  - lib/legion/mcp/tool_governance.rb
156
157
  - lib/legion/mcp/tool_quality.rb
157
158
  - lib/legion/mcp/tools/absorb.rb
@@ -218,6 +219,7 @@ files:
218
219
  - lib/legion/mcp/tools/update_schedule.rb
219
220
  - lib/legion/mcp/tools/worker_costs.rb
220
221
  - lib/legion/mcp/tools/worker_lifecycle.rb
222
+ - lib/legion/mcp/tools_loader.rb
221
223
  - lib/legion/mcp/usage_filter.rb
222
224
  - lib/legion/mcp/version.rb
223
225
  homepage: https://github.com/LegionIO/legion-mcp