legion-mcp 0.7.3 → 0.7.4

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: 60a58443e45b7bb2ffc897534a7b092bf4d5c152b2206e4268a8a8c9ff6a1755
4
- data.tar.gz: 178682013f6738d801d382b32e85161044c2ed48a0e8cc979a0361f542fa0f70
3
+ metadata.gz: 1f74d80bf3fc37b0b0130f77f38d7b1611c3efa6ab193085022d3aa1462e3ad4
4
+ data.tar.gz: a9ca6d64d2f3c089a787258e4ac88855d9f5d149c22eef1863bfa429157b70ed
5
5
  SHA512:
6
- metadata.gz: 0b18864b7e771313db5d679018614dfd3b380fa2d55eb4f954310f90881efe04bc49c082e090db7e6b2b0b405c48a9b91d62f35ffb6a268ff64b2a9652c8f1db
7
- data.tar.gz: 26b516e3471032d36cbcc580fed82da09615be5dd0777321ed3147d020afe2260dab6175ab086a4e3cfa60471160b481a2de99c9d55e328e0925bf534e2a6b36
6
+ metadata.gz: cd0bf2a74ac997e5ce7db2bee56a6e9d821f6244ba211c41be054700bfeb7037c557079b6616403a4f188193133d2b2087f496a0ea7be43eec96694e6feed9b9
7
+ data.tar.gz: af2d74fa1eeb888753a815af65e9a4527fc00d44dfdf4941abc9955f1da539606f3284413518b0b618bcd50c71894ef5f37750154cbf3f35ae224390a89b4204
data/CHANGELOG.md CHANGED
@@ -1,19 +1,15 @@
1
1
  # legion-mcp Changelog
2
2
 
3
- ## [Unreleased]
4
-
5
- ### Added
6
- - `Legion::MCP::ToolAdapter` - wraps Tools::Base into MCP::Tool
7
- - `rebuild_tool_registry` method for dynamic tool registration
3
+ ## [0.7.4] - 2026-04-06
8
4
 
9
5
  ### 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
6
+ - `STATIC_TOOLS` renamed to `MCP_SPECIFIC_TOOLS` (6 MCP-only tools)
7
+ - `Catalog::Registry` calls replaced with `Tools::Registry` in catalog_dispatcher
14
8
 
15
9
  ### Removed
16
- - Direct tool ownership (tools live in LegionIO now)
10
+ - `CatalogBridge` module (replaced by `Tools::Registry`)
11
+ - `dynamic_tool_list`, `dispatch_catalog_tool`, `register_catalog_listener` from server.rb
12
+ - Stale spec files for catalog integration
17
13
 
18
14
  ## [0.7.2] - 2026-04-03
19
15
 
data/CLAUDE.md CHANGED
@@ -7,7 +7,7 @@
7
7
  Standalone gem providing the Model Context Protocol (MCP) server for LegionIO. Extracted from LegionIO to enable independent versioning and reuse. Includes semantic tool matching, observation pipeline, context compilation, tiered inference (Tier 0/1/2), and tool governance.
8
8
 
9
9
  **GitHub**: https://github.com/LegionIO/legion-mcp
10
- **Version**: 0.6.2
10
+ **Version**: 0.7.4
11
11
  **License**: Apache-2.0
12
12
  **Ruby**: >= 3.4
13
13
 
@@ -15,20 +15,30 @@ Standalone gem providing the Model Context Protocol (MCP) server for LegionIO. E
15
15
 
16
16
  ```
17
17
  Legion::MCP
18
- ├── Server # MCP::Server builder, TOOL_CLASSES registration, governance-aware build
18
+ ├── Server # MCP::Server builder, governance-aware build; tool list sourced from Legion::Tools::Registry via DeferredRegistry
19
19
  ├── Auth # JWT + API key authentication
20
20
  ├── ToolGovernance # Risk-tier tool filtering + invocation audit
21
21
  ├── ContextCompiler # Keyword + semantic tool matching, blended scoring (60% semantic + 40% keyword)
22
- ├── EmbeddingIndex # In-memory vector cache for semantic tool matching
22
+ ├── EmbeddingIndex # Semantic tool matching; delegates embedding persistence to Tools::EmbeddingCache (L0-L4)
23
23
  ├── Observer # Instrumentation pipeline: counters, ring buffer, pattern promotion
24
24
  ├── UsageFilter # Frequency/recency/keyword scoring for dynamic tool filtering
25
25
  ├── PatternStore # 4-layer degrading storage (L0 memory, L1 cache, L2 local SQLite)
26
26
  ├── TierRouter # Confidence-gated tier selection (Tier 0/1/2)
27
27
  ├── ContextGuard # Staleness, rapid-fire, anomaly detection guards
28
- ├── Tools/ # 59 MCP::Tool subclasses (legion.* namespace)
28
+ ├── ToolAdapter # Adapts Legion::Tools::Base subclasses to MCP SDK format (McpToolAdapter kept as alias)
29
+ ├── DeferredRegistry # Reads deferred tools from Legion::Tools::Registry at request time
30
+ ├── Tools/ # MCP_SPECIFIC_TOOLS only (6 registered); remaining tool files exist but are not registered in Server.tool_registry — extension tools discovered via Legion::Tools::Discovery
29
31
  └── Resources/ # RunnerCatalog, ExtensionInfo
30
32
  ```
31
33
 
34
+ ### Tool Registry Migration Notes
35
+
36
+ - **Before**: legion-mcp owned 57+ individual `Tools/*.rb` files registered in `TOOL_CLASSES`.
37
+ - **After**: Tools discovered dynamically via `Legion::Tools::Discovery` from extension `runner_modules` at boot. `Legion::Tools::Registry` classifies each as `:always` or `:deferred`. `DeferredRegistry` resolves the deferred set at request time.
38
+ - `MCP_SPECIFIC_TOOLS` (6 tools) covers MCP-only concerns not owned by any extension.
39
+ - `CatalogBridge` removed — bridged old `Extensions::Capability` / `Catalog::Registry` which no longer exist.
40
+ - `EmbeddingIndex` uses `Legion::Tools::EmbeddingCache` (5-tier L0–L4) instead of its own in-memory store.
41
+
32
42
  ## Dependencies
33
43
 
34
44
  | Gem | Required | Purpose |
@@ -65,17 +75,19 @@ All optional dependencies use `defined?()` guards:
65
75
  |------|---------|
66
76
  | `lib/legion/mcp.rb` | Entry point: `Legion::MCP.server` singleton factory |
67
77
  | `lib/legion/mcp/version.rb` | `Legion::MCP::VERSION` constant |
68
- | `lib/legion/mcp/server.rb` | MCP::Server builder, TOOL_CLASSES array, governance-aware build |
78
+ | `lib/legion/mcp/server.rb` | MCP::Server builder, governance-aware build; reads tools from Tools::Registry |
69
79
  | `lib/legion/mcp/auth.rb` | JWT + API key authentication |
70
80
  | `lib/legion/mcp/tool_governance.rb` | Risk-tier tool filtering + invocation audit |
71
81
  | `lib/legion/mcp/context_compiler.rb` | Keyword + semantic tool matching (60/40 blend) |
72
- | `lib/legion/mcp/embedding_index.rb` | In-memory vector cache for semantic matching |
82
+ | `lib/legion/mcp/embedding_index.rb` | Semantic tool matching; delegates persistence to Legion::Tools::EmbeddingCache |
73
83
  | `lib/legion/mcp/observer.rb` | Instrumentation: counters, ring buffer, pattern promotion |
74
84
  | `lib/legion/mcp/usage_filter.rb` | Frequency/recency/keyword scoring for dynamic tool filtering |
75
85
  | `lib/legion/mcp/pattern_store.rb` | 4-layer degrading storage (L0/L1/L2) with thread-safe access |
76
86
  | `lib/legion/mcp/tier_router.rb` | Confidence-gated tier selection, tool chain execution |
77
87
  | `lib/legion/mcp/context_guard.rb` | Staleness, rapid-fire, anomaly detection |
78
- | `lib/legion/mcp/tools/` | 59 MCP::Tool subclasses (legion.* namespace) |
88
+ | `lib/legion/mcp/tool_adapter.rb` | MCP::ToolAdapter wraps Legion::Tools::Base for MCP SDK (McpToolAdapter kept as alias) |
89
+ | `lib/legion/mcp/deferred_registry.rb` | DeferredRegistry — reads deferred tools from Legion::Tools::Registry at request time |
90
+ | `lib/legion/mcp/tools/` | All tool implementations; only MCP_SPECIFIC_TOOLS (6 tools) registered in Server.tool_registry — extension tools sourced via Legion::Tools::Discovery |
79
91
  | `lib/legion/mcp/tools/do_action.rb` | Natural language intent routing with Tier 0 fast path |
80
92
  | `lib/legion/mcp/tools/discover_tools.rb` | Dynamic tool discovery with context |
81
93
  | `lib/legion/mcp/tools/run_task.rb` | Execute runner function via dot notation |
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'logging_support'
4
+ require_relative 'tool_adapter'
4
5
 
5
6
  module Legion
6
7
  module MCP
@@ -113,35 +114,16 @@ module Legion
113
114
  end
114
115
 
115
116
  def generate_tools_from_catalog
116
- return [] unless defined?(Legion::Extensions::Catalog::Registry)
117
- return [] unless Legion::Extensions::Catalog::Registry.respond_to?(:for_mcp)
117
+ return [] unless defined?(Legion::Tools::Registry)
118
+ return [] unless Legion::Tools::Registry.respond_to?(:all_tools)
118
119
 
119
- Legion::Extensions::Catalog::Registry.for_mcp.filter_map do |cap|
120
- raw_name = cap.respond_to?(:mcp_name) ? cap.mcp_name : "legion.catalog.#{cap.function}"
121
- build_tool_class(
122
- runner_class: resolve_runner_class(cap),
123
- function: cap.function,
124
- tool_name: sanitize_tool_name(raw_name),
125
- description: cap.respond_to?(:description) ? cap.description : "Auto-generated: #{cap.function}",
126
- input_schema: cap.respond_to?(:input_schema) ? cap.input_schema : { properties: {} },
127
- category: cap.respond_to?(:category) ? cap.category : nil,
128
- tier: cap.respond_to?(:tier) ? cap.tier : nil
129
- )
120
+ Legion::Tools::Registry.all_tools.filter_map do |tool_class|
121
+ ToolAdapter.from_legion_tool(tool_class) if defined?(ToolAdapter) && ToolAdapter.respond_to?(:from_legion_tool)
130
122
  rescue StandardError => e
131
123
  handle_exception(e, level: :debug, operation: 'legion.mcp.catalog_dispatcher.generate_tools_from_catalog')
132
- log.debug("CatalogDispatcher: skipping #{cap}: #{e.message}")
133
124
  nil
134
125
  end
135
126
  end
136
-
137
- def resolve_runner_class(cap)
138
- segments = cap.extension.delete_prefix('lex-').split('-')
139
- (%w[Legion Extensions] + segments.map(&:capitalize) + ['Runners', cap.runner]).join('::')
140
- end
141
-
142
- def sanitize_tool_name(name)
143
- name.gsub(/[^A-Za-z0-9_.-]/, '')
144
- end
145
127
  end
146
128
  end
147
129
  end
@@ -17,82 +17,24 @@ require_relative 'tool_quality'
17
17
  require_relative 'deferred_registry'
18
18
  require_relative 'catalog_dispatcher'
19
19
  require_relative 'dynamic_injector'
20
- require_relative 'catalog_bridge'
21
20
  require_relative 'resources/runner_catalog'
22
21
  require_relative 'resources/extension_info'
23
22
 
24
23
  module Legion
25
24
  module MCP
26
- module Server # rubocop:disable Metrics/ModuleLength
27
- # All built-in tool classes loaded via tools_loader.rb
28
- STATIC_TOOLS = [
29
- Tools::RunTask,
30
- Tools::DescribeRunner,
31
- Tools::ListTasks,
32
- Tools::GetTask,
33
- Tools::DeleteTask,
34
- Tools::GetTaskLogs,
35
- Tools::ListChains,
36
- Tools::CreateChain,
37
- Tools::UpdateChain,
38
- Tools::DeleteChain,
39
- Tools::ListRelationships,
40
- Tools::CreateRelationship,
41
- Tools::UpdateRelationship,
42
- Tools::DeleteRelationship,
43
- Tools::ListExtensions,
44
- Tools::GetExtension,
45
- Tools::EnableExtension,
46
- Tools::DisableExtension,
47
- Tools::ListSchedules,
48
- Tools::CreateSchedule,
49
- Tools::UpdateSchedule,
50
- Tools::DeleteSchedule,
51
- Tools::GetStatus,
52
- Tools::GetConfig,
53
- Tools::ListWorkers,
54
- Tools::ShowWorker,
55
- Tools::WorkerLifecycle,
56
- Tools::WorkerCosts,
57
- Tools::TeamSummary,
58
- Tools::RoutingStats,
59
- Tools::RbacCheck,
60
- Tools::RbacAssignments,
61
- Tools::RbacGrants,
62
- Tools::PromptList,
63
- Tools::PromptShow,
64
- Tools::PromptRun,
65
- Tools::DatasetList,
66
- Tools::DatasetShow,
67
- Tools::ExperimentResults,
68
- Tools::EvalList,
69
- Tools::EvalRun,
70
- Tools::EvalResults,
71
- Tools::DoAction,
25
+ module Server
26
+ # MCP-specific tools not owned by any extension.
27
+ # All extension-owned tools are discovered via Legion::Tools::Registry.
28
+ MCP_SPECIFIC_TOOLS = [
72
29
  Tools::PlanAction,
73
30
  Tools::DiscoverTools,
74
- Tools::AskPeer,
75
- Tools::ListPeers,
76
- Tools::NotifyPeer,
77
- Tools::BroadcastPeers,
78
- Tools::MeshStatus,
79
- Tools::MindGrowthStatus,
80
- Tools::MindGrowthPropose,
81
- Tools::MindGrowthApprove,
82
- Tools::MindGrowthBuildQueue,
83
- Tools::MindGrowthCognitiveProfile,
84
- Tools::MindGrowthHealth,
85
- Tools::QueryKnowledge,
86
- Tools::KnowledgeHealth,
87
- Tools::KnowledgeContext,
88
- Tools::Absorb,
89
31
  Tools::StructuralIndexTool,
90
32
  Tools::ToolAudit,
91
33
  Tools::StateDiff,
92
34
  Tools::SearchSessions
93
35
  ].freeze
94
36
 
95
- @tool_registry = Concurrent::Array.new(STATIC_TOOLS)
37
+ @tool_registry = Concurrent::Array.new(MCP_SPECIFIC_TOOLS)
96
38
  @tool_registry_lock = Mutex.new
97
39
 
98
40
  class << self # rubocop:disable Metrics/ClassLength
@@ -100,7 +42,7 @@ module Legion
100
42
 
101
43
  def rebuild_tool_registry
102
44
  @tool_registry_lock.synchronize do
103
- @tool_registry = Concurrent::Array.new(STATIC_TOOLS)
45
+ @tool_registry = Concurrent::Array.new(MCP_SPECIFIC_TOOLS)
104
46
 
105
47
  if defined?(Legion::Tools::Registry) && Legion::Tools::Registry.respond_to?(:all_tools)
106
48
  Legion::Tools::Registry.all_tools.each do |legion_tool_class|
@@ -148,8 +90,8 @@ module Legion
148
90
  end
149
91
 
150
92
  def build(identity: nil) # rubocop:disable Metrics/MethodLength
93
+ run_function_discovery
151
94
  rebuild_tool_registry
152
- register_catalog_listener
153
95
 
154
96
  LoggingSupport.info(
155
97
  'server.build.start',
@@ -182,7 +124,6 @@ module Legion
182
124
 
183
125
  PatternStore.hydrate_from_l2 if defined?(PatternStore)
184
126
  ColdStart.load_community_patterns if defined?(ColdStart)
185
- run_function_discovery
186
127
  populate_embedding_index
187
128
 
188
129
  Resources::RunnerCatalog.register(server)
@@ -263,48 +204,6 @@ module Legion
263
204
  end
264
205
  end
265
206
 
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
-
308
207
  private
309
208
 
310
209
  def run_function_discovery
@@ -4,9 +4,16 @@ module Legion
4
4
  module MCP
5
5
  class ToolAdapter < ::MCP::Tool
6
6
  class << self
7
+ MCP_NAME_PATTERN = /[^a-zA-Z0-9_-]/
8
+
9
+ def sanitize_tool_name(name)
10
+ name.to_s.gsub(MCP_NAME_PATTERN, '_').slice(0, 64)
11
+ end
12
+
7
13
  def from_legion_tool(tool_class)
14
+ safe_name = sanitize_tool_name(tool_class.tool_name)
8
15
  Class.new(::MCP::Tool) do
9
- tool_name tool_class.tool_name
16
+ tool_name safe_name
10
17
  description tool_class.description
11
18
  input_schema(tool_class.input_schema || { properties: {} })
12
19
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.7.3'
5
+ VERSION = '0.7.4'
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.3
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -122,7 +122,6 @@ files:
122
122
  - lib/legion/mcp.rb
123
123
  - lib/legion/mcp/actors/self_generate_cycle.rb
124
124
  - lib/legion/mcp/auth.rb
125
- - lib/legion/mcp/catalog_bridge.rb
126
125
  - lib/legion/mcp/catalog_dispatcher.rb
127
126
  - lib/legion/mcp/client.rb
128
127
  - lib/legion/mcp/client/connection.rb
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Legion
4
- module MCP
5
- module CatalogBridge
6
- include Legion::Logging::Helper
7
-
8
- def hydrate_override_confidence
9
- return unless defined?(Legion::LLM::OverrideConfidence)
10
- return unless Legion::LLM::OverrideConfidence.respond_to?(:hydrate_from_l2)
11
-
12
- Legion::LLM::OverrideConfidence.hydrate_from_l2
13
- Legion::LLM::OverrideConfidence.hydrate_from_apollo if Legion::LLM::OverrideConfidence.respond_to?(:hydrate_from_apollo)
14
- end
15
-
16
- def register_catalog_listener
17
- return unless defined?(Legion::Extensions::Catalog::Registry)
18
- return unless Legion::Extensions::Catalog::Registry.respond_to?(:on_change)
19
-
20
- Legion::Extensions::Catalog::Registry.on_change { Legion::MCP.reset! }
21
- end
22
-
23
- def dispatch_catalog_tool(tool_name, arguments)
24
- log.info('Starting legion.mcp.catalog_bridge.dispatch_catalog_tool')
25
- return nil unless defined?(Legion::Extensions::Catalog::Registry)
26
-
27
- cap = Legion::Extensions::Catalog::Registry.find_by_mcp_name(tool_name)
28
- return nil unless cap
29
-
30
- segments = cap.extension.delete_prefix('lex-').split('-')
31
- runner_path = (%w[Legion Extensions] + segments.map(&:capitalize) + ['Runners', cap.runner]).join('::')
32
- runner = Kernel.const_get(runner_path)
33
- fn = cap.function.to_sym
34
- result = runner.send(fn, **(arguments || {}).transform_keys(&:to_sym))
35
- { status: :success, result: result, source: :catalog }
36
- rescue NameError => e
37
- handle_exception(e, level: :warn, operation: 'legion.mcp.catalog_bridge.dispatch_catalog_tool')
38
- log.warn("Catalog dispatch failed: #{e.message}")
39
- nil
40
- rescue StandardError => e
41
- handle_exception(e, level: :error, operation: 'legion.mcp.catalog_bridge.dispatch_catalog_tool')
42
- { status: :error, error: e.message, source: :catalog }
43
- end
44
-
45
- def register_catalog_tools
46
- log.info('Starting legion.mcp.catalog_bridge.register_catalog_tools')
47
- CatalogDispatcher.generate_tools_from_catalog.each { |tc| Server.register_tool(tc) }
48
- end
49
-
50
- def dynamic_tool_list
51
- static = Server.tool_registry.map do |klass|
52
- { name: klass.tool_name, description: klass.description,
53
- input_schema: klass.input_schema, source: :builtin, klass: klass }
54
- end
55
-
56
- dynamic = if defined?(Legion::Extensions::Catalog::Registry)
57
- Legion::Extensions::Catalog::Registry.for_mcp.map(&:to_mcp_tool)
58
- else
59
- []
60
- end
61
-
62
- static + dynamic
63
- end
64
- end
65
- end
66
- end