legion-mcp 0.6.6 → 0.7.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: 3e1c1371cb216ffb1b47a924ec9fd5d65b352b94a87e0103ef28d2f96aee77b1
4
- data.tar.gz: 0e96367cb8c743f2408f95c9ccdf051ecd1ba991302963b4e5b5c500db5eb136
3
+ metadata.gz: ee9fe829857bd0a2546ea95daf827d33fe60084094e665a78f7fe78742064978
4
+ data.tar.gz: cc57fd55347d9a541b382d7ca8a92d4a2d9094e674142e4336cfaadf46f4071c
5
5
  SHA512:
6
- metadata.gz: ab78e2afd0d736017b922a12010c3feb9ca519a82599e163e6cdf187d448978b40a62a5efc2f0ebe7991424af5593effa6dfe89c17192eb721b809216ba80f01
7
- data.tar.gz: 4ee7a30af588af08bf7090748dd700ff40863150bf4cbdaf147e5f073cb60c7bff82ac26758c650e325c4833b24fc5a2e4bd17ca341c8bbfede8e24e9d037677
6
+ metadata.gz: 15839e1d0bda42420e4734eef4a550a8b8b163453ec68a5eaf75aeb33b1d931625a5da8b30449db7627d2965ad7306d2a2c00633f559895ee442da35e5d5cee4
7
+ data.tar.gz: 77b1a31f1c5a24bc4c36b537500dfe849f9ff413a14de994032551d3161e52dee33729bd292ebb1a69fbe2625eeda682c985aa0c3937ed9d97ea30da7d0049d9
data/AGENTS.md ADDED
@@ -0,0 +1,37 @@
1
+ # legion-mcp Agent Notes
2
+
3
+ ## Scope
4
+
5
+ `legion-mcp` is the standalone MCP server gem for Legion. It owns tool/resource registration, tier routing, tool governance, semantic matching, and observation/pattern promotion.
6
+
7
+ ## Fast Start
8
+
9
+ ```bash
10
+ bundle install
11
+ bundle exec rspec
12
+ bundle exec rubocop -A
13
+ bundle exec rubocop
14
+ ```
15
+
16
+ ## Primary Entry Points
17
+
18
+ - `lib/legion/mcp.rb`
19
+ - `lib/legion/mcp/server.rb`
20
+ - `lib/legion/mcp/context_compiler.rb`
21
+ - `lib/legion/mcp/tier_router.rb`
22
+ - `lib/legion/mcp/pattern_store.rb`
23
+ - `lib/legion/mcp/observer.rb`
24
+ - `lib/legion/mcp/tools/`
25
+
26
+ ## Guardrails
27
+
28
+ - Keep optional dependencies guarded (`legion-cache`, `legion-llm`, `Data::Local`); this gem must degrade cleanly.
29
+ - Tier confidence behavior is core contract: Tier 0 (>= 0.8), Tier 1 (0.6-0.8), Tier 2 (< 0.6).
30
+ - Pattern storage failures must not block tool execution.
31
+ - Tool registration remains centralized through server builder/registry; avoid ad hoc registration paths.
32
+ - Preserve governance checks and audit trails for tool invocation.
33
+
34
+ ## Validation
35
+
36
+ - Run specs for changed tools/router/compiler paths.
37
+ - Before handoff, run `bundle exec rspec`, `bundle exec rubocop -A`, then `bundle exec rubocop`.
data/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.0] - 2026-03-31
6
+
7
+ ### Added
8
+ - `DeferredRegistry` module for deferred tool loading — tools not in the always-loaded set return name and description only (no `inputSchema`) in `tools/list`, reducing token footprint by ~75% for standard MCP clients (closes #19)
9
+ - `legion.tools` now accepts `tool_names` (array) + `schema: true` parameters to load full JSON schemas for specific deferred tools on demand
10
+ - `Settings.deferred_loading_defaults` — configurable `enabled` (default true) and `always_loaded` (custom tool names merged with built-in defaults)
11
+ - 13 always-loaded tools: `legion.do`, `legion.tools`, `legion.run_task`, `legion.list_tasks`, `legion.get_task`, `legion.get_status`, `legion.describe_runner`, `legion.plan_action`, `legion.query_knowledge`, `legion.knowledge_context`, `legion.knowledge_health`, `legion.absorb`, `legion.get_task_logs`
12
+
13
+ - `CatalogDispatcher` module — thin dispatch layer routing MCP tool calls through `Legion::Ingress` for RBAC, audit, and sandbox enforcement; auto-generates tool classes from `Catalog::Registry` entries (closes #20)
14
+ - `DynamicInjector` module — context-aware tool injection/removal using `ContextCompiler.match_tools`; sends `notifications/tools/list_changed` when active tool set changes based on conversation context
15
+ - `CatalogBridge.register_catalog_tools` — auto-generates and registers catalog-sourced tools through `CatalogDispatcher` at server boot
16
+ - `Settings.dynamic_tools_defaults` — configurable `enabled` (default false) and `max_injected` (default 10)
17
+ - `StructuralIndex` module — precomputed static index of all extensions, runners, actors, and tools with JSON cache at `~/.legionio/cache/structural_index.json`; supports filtering by extension name or type (closes #18)
18
+ - `legion.structural_index` MCP tool (61st tool) — query the structural index with optional `extension`, `type`, and `refresh` parameters
19
+ - `ToolQuality` module — docstring quality gate (min description length, param descriptions), category resolution across `CATEGORIES` and `EXPANDED_CATEGORIES`, reads/writes capability matrix, and audit summary (closes #17)
20
+ - `legion.tool_audit` MCP tool (62nd tool) — audit all registered tools with modes: `summary` (default), `matrix` (capability matrix), `issues` (quality warnings only)
21
+ - `ContextCompiler::CATEGORIES` expanded from 9 to 16 categories: added `knowledge`, `mesh`, `mind_growth`, `prompts`, `datasets`, `evals`, `meta` — all 62 tools now have category assignments
22
+ - `StateTracker` module — in-memory state snapshots with timestamps and delta diff computation; tracks tool count, observer stats, pattern count, and extension count (closes #16)
23
+ - `legion.state_diff` MCP tool (63rd tool) — return only changed system state since a given timestamp; supports `snapshot: true` to take a baseline and `since:` for delta polling
24
+ - `legion.search_sessions` MCP tool (64th tool) — search across past conversation sessions by keyword or topic with relevance-sorted results and context snippets (closes #15)
25
+
26
+ ### Changed
27
+ - `Server.build` now installs a custom `tools/list` handler via `install_deferred_tools_list_handler` for mcp gem 0.10 compatibility (replaces removed `tools_list_handler` block API)
28
+ - `Server.build` now calls `register_catalog_tools` to auto-generate Ingress-dispatched tool classes from Catalog entries
29
+
5
30
  ## [0.6.6] - 2026-03-28
6
31
 
7
32
  ### Added
data/Gemfile CHANGED
@@ -6,5 +6,6 @@ gemspec
6
6
  gem 'rake'
7
7
  gem 'rspec'
8
8
  gem 'rubocop'
9
+ gem 'rubocop-legion'
9
10
  gem 'rubocop-rspec'
10
11
  gem 'simplecov'
@@ -37,6 +37,10 @@ module Legion
37
37
  { status: :error, error: e.message, source: :catalog }
38
38
  end
39
39
 
40
+ def register_catalog_tools
41
+ CatalogDispatcher.generate_tools_from_catalog.each { |tc| Server.register_tool(tc) }
42
+ end
43
+
40
44
  def dynamic_tool_list
41
45
  static = Server.tool_registry.map do |klass|
42
46
  { name: klass.tool_name, description: klass.description,
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module CatalogDispatcher
6
+ module_function
7
+
8
+ def dispatch(runner_class:, function:, params:, source: :mcp)
9
+ return nil unless defined?(Legion::Ingress)
10
+
11
+ Legion::Ingress.run(
12
+ payload: params,
13
+ runner_class: runner_class,
14
+ function: function.to_sym,
15
+ source: source,
16
+ check_subtask: true,
17
+ generate_task: true
18
+ )
19
+ end
20
+
21
+ def build_tool_class(entry)
22
+ runner_class_str = entry[:runner_class]
23
+ function_name = entry[:function]
24
+ tool_name_val = entry[:tool_name]
25
+ desc = entry[:description]
26
+ schema = entry[:input_schema] || { properties: {} }
27
+ category = entry[:category]
28
+ tier = entry[:tier]
29
+
30
+ klass = Class.new(::MCP::Tool) do
31
+ tool_name tool_name_val
32
+ description desc
33
+ input_schema(schema)
34
+ define_singleton_method(:mcp_category) { category }
35
+ define_singleton_method(:mcp_tier) { tier }
36
+ define_singleton_method(:catalog_entry) { true }
37
+ end
38
+
39
+ wire_dispatch(klass, runner_class_str, function_name)
40
+ klass
41
+ end
42
+
43
+ def wire_dispatch(klass, runner_class_str, function_name)
44
+ klass.define_singleton_method(:call) do |**params|
45
+ result = CatalogDispatcher.dispatch(
46
+ runner_class: runner_class_str,
47
+ function: function_name,
48
+ params: params
49
+ )
50
+
51
+ if result.nil?
52
+ text = Legion::JSON.dump({ error: 'Ingress not available' })
53
+ ::MCP::Tool::Response.new([{ type: 'text', text: text }], error: true)
54
+ else
55
+ text = defined?(Legion::JSON) ? Legion::JSON.dump(result) : result.to_s
56
+ ::MCP::Tool::Response.new([{ type: 'text', text: text }])
57
+ end
58
+ rescue StandardError => e
59
+ Legion::Logging.warn("CatalogDispatcher: #{function_name} failed: #{e.message}") if defined?(Legion::Logging)
60
+ text = Legion::JSON.dump({ error: e.message })
61
+ ::MCP::Tool::Response.new([{ type: 'text', text: text }], error: true)
62
+ end
63
+ end
64
+
65
+ def generate_tools_from_catalog
66
+ return [] unless defined?(Legion::Extensions::Catalog::Registry)
67
+ return [] unless Legion::Extensions::Catalog::Registry.respond_to?(:for_mcp)
68
+
69
+ Legion::Extensions::Catalog::Registry.for_mcp.filter_map do |cap|
70
+ build_tool_class(
71
+ runner_class: resolve_runner_class(cap),
72
+ function: cap.function,
73
+ tool_name: cap.respond_to?(:mcp_name) ? cap.mcp_name : "legion.catalog.#{cap.function}",
74
+ description: cap.respond_to?(:description) ? cap.description : "Auto-generated: #{cap.function}",
75
+ input_schema: cap.respond_to?(:input_schema) ? cap.input_schema : { properties: {} },
76
+ category: cap.respond_to?(:category) ? cap.category : nil,
77
+ tier: cap.respond_to?(:tier) ? cap.tier : nil
78
+ )
79
+ rescue StandardError => e
80
+ Legion::Logging.debug("CatalogDispatcher: skipping #{cap}: #{e.message}") if defined?(Legion::Logging)
81
+ nil
82
+ end
83
+ end
84
+
85
+ def resolve_runner_class(cap)
86
+ segments = cap.extension.delete_prefix('lex-').split('-')
87
+ (%w[Legion Extensions] + segments.map(&:capitalize) + ['Runners', cap.runner]).join('::')
88
+ end
89
+ end
90
+ end
91
+ end
@@ -43,6 +43,37 @@ module Legion
43
43
  describe: {
44
44
  tools: %w[legion.describe_runner],
45
45
  summary: 'Inspect a specific runner function - parameters, return type, metadata.'
46
+ },
47
+ knowledge: {
48
+ tools: %w[legion.query_knowledge legion.knowledge_health legion.knowledge_context legion.absorb],
49
+ summary: 'Knowledge base operations - query, health, context retrieval, content absorption.'
50
+ },
51
+ mesh: {
52
+ tools: %w[legion.ask_peer legion.list_peers legion.notify_peer legion.broadcast_peers
53
+ legion.mesh_status],
54
+ summary: 'Agent mesh communication - peer queries, notifications, broadcasts, and topology.'
55
+ },
56
+ mind_growth: {
57
+ tools: %w[legion.mind_growth_status legion.mind_growth_propose legion.mind_growth_approve
58
+ legion.mind_growth_build_queue legion.mind_growth_cognitive_profile
59
+ legion.mind_growth_health],
60
+ summary: 'Cognitive growth - proposals, approvals, build queue, profiling, fitness scores.'
61
+ },
62
+ prompts: {
63
+ tools: %w[legion.prompt_list legion.prompt_show legion.prompt_run],
64
+ summary: 'Prompt template management - list, view, and render prompt templates.'
65
+ },
66
+ datasets: {
67
+ tools: %w[legion.dataset_list legion.dataset_show legion.experiment_results],
68
+ summary: 'Dataset and experiment browsing - list datasets, view rows, compare results.'
69
+ },
70
+ evals: {
71
+ tools: %w[legion.eval_list legion.eval_run legion.eval_results],
72
+ summary: 'Evaluation management - list evaluators, run evaluations, view results.'
73
+ },
74
+ meta: {
75
+ tools: %w[legion.do legion.tools legion.plan_action legion.structural_index],
76
+ summary: 'Meta-tools - natural language routing, tool discovery, planning, structural index.'
46
77
  }
47
78
  }.freeze
48
79
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module DeferredRegistry
6
+ # Tools that are ALWAYS fully loaded (never deferred).
7
+ # These are high-frequency entry points or meta-tools.
8
+ ALWAYS_LOADED = %w[
9
+ legion.do
10
+ legion.tools
11
+ legion.run_task
12
+ legion.list_tasks
13
+ legion.get_task
14
+ legion.get_status
15
+ legion.describe_runner
16
+ legion.plan_action
17
+ legion.query_knowledge
18
+ legion.knowledge_context
19
+ legion.knowledge_health
20
+ legion.absorb
21
+ legion.get_task_logs
22
+ ].freeze
23
+
24
+ module_function
25
+
26
+ def enabled?
27
+ setting = Legion::Settings.dig(:mcp, :deferred_loading, :enabled)
28
+ setting.nil? || setting
29
+ end
30
+
31
+ def always_loaded_tools
32
+ custom = Legion::Settings.dig(:mcp, :deferred_loading, :always_loaded)
33
+ custom.is_a?(Array) ? (ALWAYS_LOADED | custom) : ALWAYS_LOADED
34
+ end
35
+
36
+ def deferred?(tool_class)
37
+ return false unless enabled?
38
+
39
+ name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name
40
+ !always_loaded_tools.include?(name)
41
+ end
42
+
43
+ def deferred_entry(tool_class)
44
+ { name: tool_class.tool_name, description: tool_class.description }
45
+ end
46
+
47
+ def full_entry(tool_class)
48
+ tool_class.to_h
49
+ end
50
+
51
+ def build_tools_list(tool_classes)
52
+ tool_classes.map do |tc|
53
+ if deferred?(tc)
54
+ deferred_entry(tc)
55
+ else
56
+ full_entry(tc)
57
+ end
58
+ end
59
+ end
60
+
61
+ def resolve_schemas(tool_names, tool_classes)
62
+ tool_names.filter_map do |name|
63
+ tc = tool_classes.find { |klass| klass.tool_name == name }
64
+ next unless tc
65
+
66
+ tc.to_h
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module DynamicInjector
6
+ MAX_INJECTED = 10
7
+
8
+ module_function
9
+
10
+ def enabled?
11
+ Legion::Settings.dig(:mcp, :dynamic_tools, :enabled) == true
12
+ end
13
+
14
+ def max_injected
15
+ Legion::Settings.dig(:mcp, :dynamic_tools, :max_injected) || MAX_INJECTED
16
+ end
17
+
18
+ def context_tools(intent_string)
19
+ return [] unless enabled?
20
+ return [] if intent_string.nil? || intent_string.strip.empty?
21
+
22
+ matches = ContextCompiler.match_tools(intent_string, limit: max_injected)
23
+ return [] if matches.empty?
24
+
25
+ always = DeferredRegistry.always_loaded_tools
26
+ matches.filter_map do |match|
27
+ next if always.include?(match[:name])
28
+ next if match[:score] <= 0
29
+
30
+ Server.tool_registry.find { |tc| tc.tool_name == match[:name] }
31
+ end
32
+ end
33
+
34
+ def active_tool_set(intent_string)
35
+ always = always_loaded_classes
36
+ injected = context_tools(intent_string)
37
+ (always + injected).uniq(&:tool_name)
38
+ end
39
+
40
+ def always_loaded_classes
41
+ names = DeferredRegistry.always_loaded_tools
42
+ Server.tool_registry.select { |tc| names.include?(tc.tool_name) }
43
+ end
44
+
45
+ def tools_changed?(previous_names, current_names)
46
+ previous_names.sort != current_names.sort
47
+ end
48
+
49
+ def notify_if_changed(server, previous_names, current_names)
50
+ return unless tools_changed?(previous_names, current_names)
51
+ return unless server.respond_to?(:notify_tools_list_changed)
52
+
53
+ server.notify_tools_list_changed
54
+ rescue StandardError => e
55
+ Legion::Logging.debug("DynamicInjector: notify failed: #{e.message}") if defined?(Legion::Logging)
56
+ end
57
+
58
+ def inject_for_context(server, intent_string, previous_names: [])
59
+ return previous_names unless enabled?
60
+
61
+ tools = active_tool_set(intent_string)
62
+ current_names = tools.map(&:tool_name)
63
+
64
+ notify_if_changed(server, previous_names, current_names)
65
+ current_names
66
+ end
67
+ end
68
+ end
69
+ end
@@ -68,6 +68,16 @@ require_relative 'tools/query_knowledge'
68
68
  require_relative 'tools/knowledge_health'
69
69
  require_relative 'tools/knowledge_context'
70
70
  require_relative 'tools/absorb'
71
+ require_relative 'tools/structural_index'
72
+ require_relative 'tools/tool_audit'
73
+ require_relative 'tools/state_diff'
74
+ require_relative 'tools/search_sessions'
75
+ require_relative 'structural_index'
76
+ require_relative 'state_tracker'
77
+ require_relative 'tool_quality'
78
+ require_relative 'deferred_registry'
79
+ require_relative 'catalog_dispatcher'
80
+ require_relative 'dynamic_injector'
71
81
  require_relative 'catalog_bridge'
72
82
  require_relative 'resources/runner_catalog'
73
83
  require_relative 'resources/extension_info'
@@ -135,7 +145,11 @@ module Legion
135
145
  Tools::QueryKnowledge,
136
146
  Tools::KnowledgeHealth,
137
147
  Tools::KnowledgeContext,
138
- Tools::Absorb
148
+ Tools::Absorb,
149
+ Tools::StructuralIndexTool,
150
+ Tools::ToolAudit,
151
+ Tools::StateDiff,
152
+ Tools::SearchSessions
139
153
  ].freeze
140
154
 
141
155
  @tool_registry = Concurrent::Array.new(STATIC_TOOLS)
@@ -189,21 +203,12 @@ module Legion
189
203
  end
190
204
  end
191
205
 
192
- server.tools_list_handler do |_params|
193
- build_filtered_tool_list.map(&:to_h)
194
- end
206
+ install_deferred_tools_list_handler(server)
195
207
 
196
- # Hydrate pattern store from L2 persistence (SQLite) on boot
197
208
  PatternStore.hydrate_from_l2 if defined?(PatternStore)
198
-
199
- # Cold-start: load community patterns if store is still empty after hydration
200
209
  ColdStart.load_community_patterns if defined?(ColdStart)
201
-
202
- # Discover and register runner functions before building the embedding index
203
- # so all tools are present when embeddings are populated
204
210
  FunctionDiscovery.discover_and_register if defined?(Legion::Extensions)
205
-
206
- # Populate embedding index for semantic tool matching (lazy — no-op if LLM unavailable)
211
+ register_catalog_tools
207
212
  populate_embedding_index
208
213
 
209
214
  Resources::RunnerCatalog.register(server)
@@ -262,6 +267,15 @@ module Legion
262
267
 
263
268
  private
264
269
 
270
+ def install_deferred_tools_list_handler(server)
271
+ handlers = server.instance_variable_get(:@handlers)
272
+ return unless handlers
273
+
274
+ handlers[::MCP::Methods::TOOLS_LIST] = lambda { |_request|
275
+ DeferredRegistry.build_tools_list(build_filtered_tool_list)
276
+ }
277
+ end
278
+
265
279
  def instructions
266
280
  <<~TEXT
267
281
  Legion is an async job engine. You can run tasks, create chains and relationships between services, manage extensions, and query system status.
@@ -13,10 +13,19 @@ module Legion
13
13
  connect_timeout: 10,
14
14
  call_timeout: 30,
15
15
  codegen: { self_generate: self_generate_defaults },
16
- mcp: { auto_expose_runners: false }
16
+ mcp: { auto_expose_runners: false, deferred_loading: deferred_loading_defaults,
17
+ dynamic_tools: dynamic_tools_defaults }
17
18
  }
18
19
  end
19
20
 
21
+ def deferred_loading_defaults
22
+ { enabled: true, always_loaded: [] }
23
+ end
24
+
25
+ def dynamic_tools_defaults
26
+ { enabled: false, max_injected: 10 }
27
+ end
28
+
20
29
  def self_generate_defaults
21
30
  {
22
31
  enabled: false,
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module StateTracker
6
+ MAX_SNAPSHOTS = 50
7
+
8
+ module_function
9
+
10
+ def snapshot
11
+ state = collect_state
12
+ timestamp = Time.now.floor
13
+
14
+ snapshots_mutex.synchronize do
15
+ snapshots << { state: state, timestamp: timestamp }
16
+ snapshots.shift if snapshots.size > MAX_SNAPSHOTS
17
+ end
18
+
19
+ { state: state, timestamp: timestamp.iso8601 }
20
+ end
21
+
22
+ def diff(since:)
23
+ since_time = parse_time(since)
24
+ return { error: 'invalid timestamp' } unless since_time
25
+
26
+ baseline = find_baseline(since_time)
27
+ current = collect_state
28
+
29
+ if baseline.nil?
30
+ return { full_state: current, reason: 'no baseline found for given timestamp',
31
+ timestamp: Time.now.iso8601 }
32
+ end
33
+
34
+ changes = compute_diff(baseline[:state], current)
35
+ { changes: changes, since: since_time.iso8601, timestamp: Time.now.iso8601 }
36
+ end
37
+
38
+ def collect_state
39
+ {
40
+ tool_count: Server.tool_registry.size,
41
+ observer_stats: collect_observer_stats,
42
+ pattern_count: collect_pattern_count,
43
+ extensions: collect_extension_count
44
+ }
45
+ end
46
+
47
+ def collect_observer_stats
48
+ return {} unless defined?(Observer)
49
+
50
+ stats = Observer.stats
51
+ { total_calls: stats[:total_calls], tool_count: stats[:tool_count], failure_rate: stats[:failure_rate] }
52
+ rescue StandardError
53
+ {}
54
+ end
55
+
56
+ def collect_pattern_count
57
+ return 0 unless defined?(PatternStore)
58
+
59
+ PatternStore.respond_to?(:size) ? PatternStore.size : 0
60
+ rescue StandardError
61
+ 0
62
+ end
63
+
64
+ def collect_extension_count
65
+ return 0 unless defined?(Legion::Extensions)
66
+
67
+ extensions = if Legion::Extensions.respond_to?(:extensions)
68
+ Legion::Extensions.extensions
69
+ else
70
+ Legion::Extensions.instance_variable_get(:@extensions)
71
+ end
72
+ extensions&.size || 0
73
+ rescue StandardError
74
+ 0
75
+ end
76
+
77
+ def compute_diff(baseline, current)
78
+ changes = {}
79
+ all_keys = (baseline.keys | current.keys).uniq
80
+
81
+ all_keys.each do |key|
82
+ old_val = baseline[key]
83
+ new_val = current[key]
84
+
85
+ next if old_val == new_val
86
+
87
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
88
+ nested = compute_diff(old_val, new_val)
89
+ changes[key] = nested unless nested.empty?
90
+ else
91
+ changes[key] = { before: old_val, after: new_val }
92
+ end
93
+ end
94
+
95
+ changes
96
+ end
97
+
98
+ def find_baseline(since_time)
99
+ snapshots_mutex.synchronize do
100
+ snapshots.reverse.find { |s| s[:timestamp] <= since_time }
101
+ end
102
+ end
103
+
104
+ def parse_time(value)
105
+ case value
106
+ when Time
107
+ value
108
+ when String
109
+ Time.parse(value)
110
+ when Numeric
111
+ Time.at(value)
112
+ end
113
+ rescue ArgumentError
114
+ nil
115
+ end
116
+
117
+ def snapshots
118
+ @snapshots ||= []
119
+ end
120
+
121
+ def snapshots_mutex
122
+ @snapshots_mutex ||= Mutex.new
123
+ end
124
+
125
+ def reset!
126
+ snapshots_mutex.synchronize { snapshots.clear }
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Legion
6
+ module MCP
7
+ module StructuralIndex
8
+ CACHE_PATH = File.expand_path('~/.legionio/cache/structural_index.json')
9
+
10
+ module_function
11
+
12
+ def build
13
+ {
14
+ extensions: scan_extensions,
15
+ tools: scan_tools,
16
+ generated_at: Time.now.iso8601
17
+ }
18
+ end
19
+
20
+ def scan_extensions
21
+ return [] unless defined?(Legion::Extensions)
22
+
23
+ extensions = if Legion::Extensions.respond_to?(:extensions)
24
+ Legion::Extensions.extensions || []
25
+ else
26
+ Legion::Extensions.instance_variable_get(:@extensions) || []
27
+ end
28
+
29
+ extensions.filter_map do |ext|
30
+ build_extension_entry(ext)
31
+ rescue StandardError => e
32
+ Legion::Logging.debug("StructuralIndex: skipping #{ext}: #{e.message}") if defined?(Legion::Logging)
33
+ nil
34
+ end
35
+ end
36
+
37
+ def build_extension_entry(ext)
38
+ runners = if ext.respond_to?(:runner_modules)
39
+ ext.runner_modules.filter_map { |rm| build_runner_entry(rm) }
40
+ else
41
+ []
42
+ end
43
+
44
+ actors = if ext.respond_to?(:actor_modules)
45
+ ext.actor_modules.filter_map { |am| build_actor_entry(am) }
46
+ else
47
+ []
48
+ end
49
+
50
+ name = ext.respond_to?(:extension_name) ? ext.extension_name : ext.class.name
51
+
52
+ {
53
+ name: name,
54
+ runners: runners,
55
+ actors: actors
56
+ }
57
+ end
58
+
59
+ def build_runner_entry(runner_mod)
60
+ settings = runner_mod.respond_to?(:settings) ? runner_mod.settings : {}
61
+ functions = settings.is_a?(Hash) ? (settings[:functions] || {}) : {}
62
+
63
+ {
64
+ name: runner_mod.respond_to?(:name) ? runner_mod.name : runner_mod.to_s,
65
+ functions: functions.keys.map(&:to_s)
66
+ }
67
+ end
68
+
69
+ def build_actor_entry(actor_mod)
70
+ {
71
+ name: actor_mod.respond_to?(:name) ? actor_mod.name : actor_mod.to_s,
72
+ type: actor_mod.respond_to?(:actor_type) ? actor_mod.actor_type : 'unknown'
73
+ }
74
+ end
75
+
76
+ def scan_tools
77
+ Server.tool_registry.map do |tc|
78
+ {
79
+ name: tc.tool_name,
80
+ description: tc.description,
81
+ catalog: tc.respond_to?(:catalog_entry) && tc.catalog_entry ? true : false
82
+ }
83
+ end
84
+ end
85
+
86
+ def cached
87
+ return nil unless File.exist?(CACHE_PATH)
88
+
89
+ data = File.read(CACHE_PATH)
90
+ Legion::JSON.load(data)
91
+ rescue StandardError
92
+ nil
93
+ end
94
+
95
+ def save_cache(index = nil)
96
+ index ||= build
97
+ dir = File.dirname(CACHE_PATH)
98
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
99
+ File.write(CACHE_PATH, Legion::JSON.dump(index))
100
+ index
101
+ end
102
+
103
+ def invalidate_cache
104
+ FileUtils.rm_f(CACHE_PATH)
105
+ end
106
+
107
+ def load_or_build
108
+ cached || save_cache(build)
109
+ end
110
+
111
+ def filter(index, extension: nil, type: nil)
112
+ result = index.dup
113
+ result[:extensions] = result[:extensions]&.select { |e| e[:name]&.include?(extension) } || [] if extension
114
+ apply_type_filter(result, type) if type
115
+ result
116
+ end
117
+
118
+ def apply_type_filter(result, type)
119
+ case type
120
+ when 'tools'
121
+ result.delete(:extensions)
122
+ when 'extensions'
123
+ result.delete(:tools)
124
+ when 'runners'
125
+ result[:extensions]&.each { |e| e.delete(:actors) }
126
+ result.delete(:tools)
127
+ when 'actors'
128
+ result[:extensions]&.each { |e| e.delete(:runners) }
129
+ result.delete(:tools)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module ToolQuality
6
+ MIN_DESCRIPTION_LENGTH = 20
7
+ MIN_PARAM_DESCRIPTION_LENGTH = 5
8
+
9
+ module_function
10
+
11
+ def audit_all
12
+ Server.tool_registry.map { |tc| audit_tool(tc) }
13
+ end
14
+
15
+ def audit_tool(tool_class)
16
+ issues = []
17
+ issues.concat(check_description(tool_class))
18
+ issues.concat(check_params(tool_class))
19
+
20
+ {
21
+ name: tool_class.tool_name,
22
+ description: tool_class.description,
23
+ category: resolve_category(tool_class),
24
+ issues: issues,
25
+ quality: issues.empty? ? :pass : :warn
26
+ }
27
+ end
28
+
29
+ def check_description(tool_class)
30
+ issues = []
31
+ desc = tool_class.description.to_s
32
+ issues << 'description missing' if desc.empty?
33
+ issues << "description too short (#{desc.length} chars, min #{MIN_DESCRIPTION_LENGTH})" if desc.length < MIN_DESCRIPTION_LENGTH
34
+ issues
35
+ end
36
+
37
+ def check_params(tool_class)
38
+ issues = []
39
+ raw_schema = tool_class.input_schema
40
+ schema = raw_schema.is_a?(Hash) ? raw_schema : raw_schema.to_h
41
+ properties = schema[:properties] || {}
42
+
43
+ properties.each do |param_name, meta|
44
+ meta_hash = meta.is_a?(Hash) ? meta : {}
45
+ desc = meta_hash[:description].to_s
46
+ issues << "param '#{param_name}' missing or short description" if desc.length < MIN_PARAM_DESCRIPTION_LENGTH
47
+ end
48
+
49
+ issues
50
+ end
51
+
52
+ def resolve_category(tool_class)
53
+ return tool_class.mcp_category.to_sym if tool_class.respond_to?(:mcp_category) && tool_class.mcp_category
54
+
55
+ ContextCompiler::CATEGORIES.each do |cat, config|
56
+ return cat if config[:tools].include?(tool_class.tool_name)
57
+ end
58
+
59
+ EXPANDED_CATEGORIES.each do |cat, config|
60
+ return cat if config[:tools].include?(tool_class.tool_name)
61
+ end
62
+
63
+ :uncategorized
64
+ end
65
+
66
+ def capability_matrix
67
+ Server.tool_registry.map do |tc|
68
+ raw_schema = tc.input_schema
69
+ schema = raw_schema.is_a?(Hash) ? raw_schema : raw_schema.to_h
70
+ properties = schema[:properties] || {}
71
+ required = schema[:required] || []
72
+
73
+ {
74
+ name: tc.tool_name,
75
+ category: resolve_category(tc),
76
+ param_count: properties.size,
77
+ required: required.map(&:to_s),
78
+ reads: reads?(tc),
79
+ writes: writes?(tc),
80
+ catalog: tc.respond_to?(:catalog_entry) && tc.catalog_entry
81
+ }
82
+ end
83
+ end
84
+
85
+ def reads?(tool_class)
86
+ name = tool_class.tool_name
87
+ name.start_with?('legion.list_', 'legion.get_', 'legion.show_') ||
88
+ name.include?('query') || name.include?('search') ||
89
+ name.include?('status') || name.include?('health') ||
90
+ name.include?('stats') || name.include?('describe')
91
+ end
92
+
93
+ def writes?(tool_class)
94
+ name = tool_class.tool_name
95
+ name.start_with?('legion.create_', 'legion.update_', 'legion.delete_') ||
96
+ name.start_with?('legion.enable_', 'legion.disable_') ||
97
+ name.include?('run') || name.include?('approve') ||
98
+ name.include?('propose') || name.include?('absorb') ||
99
+ name.include?('broadcast') || name.include?('notify')
100
+ end
101
+
102
+ def summary
103
+ results = audit_all
104
+ pass_count = results.count { |r| r[:quality] == :pass }
105
+ warn_count = results.count { |r| r[:quality] == :warn }
106
+ categories = results.group_by { |r| r[:category] }
107
+
108
+ {
109
+ total_tools: results.size,
110
+ passing: pass_count,
111
+ warnings: warn_count,
112
+ by_category: categories.transform_values(&:size),
113
+ issues: results.select { |r| r[:quality] == :warn }
114
+ }
115
+ end
116
+
117
+ EXPANDED_CATEGORIES = {
118
+ knowledge: {
119
+ tools: %w[legion.query_knowledge legion.knowledge_health legion.knowledge_context legion.absorb],
120
+ summary: 'Knowledge base operations — query, health, context retrieval, content absorption.'
121
+ },
122
+ mesh: {
123
+ tools: %w[legion.ask_peer legion.list_peers legion.notify_peer legion.broadcast_peers legion.mesh_status],
124
+ summary: 'Agent mesh communication — peer queries, notifications, broadcasts, and mesh topology.'
125
+ },
126
+ mind_growth: {
127
+ tools: %w[legion.mind_growth_status legion.mind_growth_propose legion.mind_growth_approve
128
+ legion.mind_growth_build_queue legion.mind_growth_cognitive_profile legion.mind_growth_health],
129
+ summary: 'Cognitive growth — proposals, approvals, build queue, cognitive profiling, fitness scores.'
130
+ },
131
+ prompts: {
132
+ tools: %w[legion.prompt_list legion.prompt_show legion.prompt_run],
133
+ summary: 'Prompt template management — list, view, and render prompt templates.'
134
+ },
135
+ datasets: {
136
+ tools: %w[legion.dataset_list legion.dataset_show legion.experiment_results],
137
+ summary: 'Dataset and experiment browsing — list datasets, view rows, compare experiment results.'
138
+ },
139
+ evals: {
140
+ tools: %w[legion.eval_list legion.eval_run legion.eval_results],
141
+ summary: 'Evaluation management — list evaluators, run evaluations, view results.'
142
+ },
143
+ meta: {
144
+ tools: %w[legion.do legion.tools legion.plan_action legion.structural_index],
145
+ summary: 'Meta-tools — natural language routing, tool discovery, planning, structural index.'
146
+ }
147
+ }.freeze
148
+ end
149
+ end
150
+ end
@@ -5,28 +5,37 @@ module Legion
5
5
  module Tools
6
6
  class DiscoverTools < ::MCP::Tool
7
7
  tool_name 'legion.tools'
8
- description 'Discover available Legion tools by category or intent. Returns compressed definitions to reduce context.'
8
+ description 'Discover available Legion tools by category or intent. Returns compressed definitions to reduce context. ' \
9
+ 'Use tool_names with schema: true to load full schemas for deferred tools.'
9
10
 
10
11
  input_schema(
11
12
  properties: {
12
- category: {
13
+ category: {
13
14
  type: 'string',
14
15
  description: 'Tool category: tasks, chains, relationships, extensions, schedules, workers, rbac, status, describe'
15
16
  },
16
- intent: {
17
+ intent: {
17
18
  type: 'string',
18
19
  description: 'Describe what you want to do and relevant tools will be ranked'
20
+ },
21
+ tool_names: {
22
+ type: 'array',
23
+ items: { type: 'string' },
24
+ description: 'Specific tool names to retrieve full schemas for (e.g., ["legion.ask_peer", "legion.list_peers"])'
25
+ },
26
+ schema: {
27
+ type: 'boolean',
28
+ description: 'When true with tool_names, returns full JSON schemas for the specified tools'
19
29
  }
20
30
  }
21
31
  )
22
32
 
23
33
  class << self
24
- def call(category: nil, intent: nil)
25
- if category
26
- result = ContextCompiler.category_tools(category.to_sym)
27
- return error_response("Unknown category: #{category}") if result.nil?
28
-
29
- text_response(result)
34
+ def call(category: nil, intent: nil, tool_names: nil, schema: nil)
35
+ if tool_names && schema
36
+ resolve_schemas(tool_names)
37
+ elsif category
38
+ lookup_category(category)
30
39
  elsif intent
31
40
  results = ContextCompiler.match_tools(intent, limit: 5)
32
41
  text_response({ matched_tools: results })
@@ -40,6 +49,22 @@ module Legion
40
49
 
41
50
  private
42
51
 
52
+ def resolve_schemas(tool_names)
53
+ schemas = DeferredRegistry.resolve_schemas(tool_names, Server.tool_registry)
54
+ if schemas.empty?
55
+ error_response("No tools found matching: #{tool_names.join(', ')}")
56
+ else
57
+ text_response({ schemas: schemas })
58
+ end
59
+ end
60
+
61
+ def lookup_category(category)
62
+ result = ContextCompiler.category_tools(category.to_sym)
63
+ return error_response("Unknown category: #{category}") if result.nil?
64
+
65
+ text_response(result)
66
+ end
67
+
43
68
  def text_response(data)
44
69
  ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
45
70
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class SearchSessions < ::MCP::Tool
7
+ tool_name 'legion.search_sessions'
8
+ description 'Search across past conversation sessions by keyword or topic. Returns matching ' \
9
+ 'sessions with context snippets sorted by relevance.'
10
+
11
+ SESSIONS_DIR = File.expand_path('~/.legion/sessions')
12
+
13
+ input_schema(
14
+ properties: {
15
+ query: {
16
+ type: 'string',
17
+ description: 'Search query — keywords or topic to find in past sessions'
18
+ },
19
+ limit: {
20
+ type: 'integer',
21
+ description: 'Maximum number of results to return (default 5)'
22
+ }
23
+ },
24
+ required: ['query']
25
+ )
26
+
27
+ class << self
28
+ def call(query:, limit: 5)
29
+ return error_response('query cannot be empty') if query.to_s.strip.empty?
30
+
31
+ results = search(query, limit: limit)
32
+ text_response({ query: query, results: results, total: results.size })
33
+ rescue StandardError => e
34
+ Legion::Logging.warn("SearchSessions#call failed: #{e.message}") if defined?(Legion::Logging)
35
+ error_response("Failed: #{e.message}")
36
+ end
37
+
38
+ private
39
+
40
+ def search(query, limit: 5)
41
+ sessions_dir = resolve_sessions_dir
42
+ return [] unless sessions_dir && Dir.exist?(sessions_dir)
43
+
44
+ pattern = query.downcase
45
+ matches = Dir.glob(File.join(sessions_dir, '*.json')).filter_map do |path|
46
+ match_session(path, pattern)
47
+ rescue StandardError
48
+ nil
49
+ end
50
+ matches.sort_by { |r| -r[:matches] }.first(limit)
51
+ end
52
+
53
+ def match_session(path, pattern)
54
+ data = Legion::JSON.load(File.read(path))
55
+ messages = data[:messages] || data['messages'] || []
56
+ matches = messages.count { |m| content_matches?(m, pattern) }
57
+ return nil if matches.zero?
58
+
59
+ first_match = messages.find { |m| content_matches?(m, pattern) }
60
+ context = extract_context(first_match, pattern)
61
+
62
+ {
63
+ session: data[:name] || data['name'] || File.basename(path, '.json'),
64
+ file: File.basename(path),
65
+ matches: matches,
66
+ context: context
67
+ }
68
+ end
69
+
70
+ def content_matches?(message, pattern)
71
+ content = message[:content] || message['content']
72
+ content.to_s.downcase.include?(pattern)
73
+ end
74
+
75
+ def extract_context(message, pattern)
76
+ content = (message[:content] || message['content']).to_s
77
+ idx = content.downcase.index(pattern)
78
+ return content[0..200] unless idx
79
+
80
+ start = [idx - 50, 0].max
81
+ content[start, 200]
82
+ end
83
+
84
+ def resolve_sessions_dir
85
+ custom = Legion::Settings.dig(:chat, :sessions_dir) if defined?(Legion::Settings)
86
+ custom || SESSIONS_DIR
87
+ end
88
+
89
+ def text_response(data)
90
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
91
+ end
92
+
93
+ def error_response(msg)
94
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class StateDiff < ::MCP::Tool
7
+ tool_name 'legion.state_diff'
8
+ description 'Return only changed system state since a given timestamp. Token-efficient polling ' \
9
+ 'for agents monitoring system state without re-fetching everything.'
10
+
11
+ input_schema(
12
+ properties: {
13
+ since: {
14
+ type: 'string',
15
+ description: 'ISO 8601 timestamp to diff against (e.g., "2026-03-31T12:00:00Z")'
16
+ },
17
+ snapshot: {
18
+ type: 'boolean',
19
+ description: 'When true, takes a state snapshot and returns it (use before polling with since:)'
20
+ }
21
+ }
22
+ )
23
+
24
+ class << self
25
+ def call(since: nil, snapshot: nil)
26
+ if snapshot
27
+ result = StateTracker.snapshot
28
+ text_response(result)
29
+ elsif since
30
+ result = StateTracker.diff(since: since)
31
+ text_response(result)
32
+ else
33
+ text_response(StateTracker.collect_state.merge(timestamp: Time.now.iso8601))
34
+ end
35
+ rescue StandardError => e
36
+ Legion::Logging.warn("StateDiff#call failed: #{e.message}") if defined?(Legion::Logging)
37
+ error_response("Failed: #{e.message}")
38
+ end
39
+
40
+ private
41
+
42
+ def text_response(data)
43
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
44
+ end
45
+
46
+ def error_response(msg)
47
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class StructuralIndexTool < ::MCP::Tool
7
+ tool_name 'legion.structural_index'
8
+ description 'Return the precomputed structural index of all extensions, runners, actors, and tools. ' \
9
+ 'Filter by extension name or type (tools, extensions, runners, actors).'
10
+
11
+ input_schema(
12
+ properties: {
13
+ extension: {
14
+ type: 'string',
15
+ description: 'Filter by extension name (partial match)'
16
+ },
17
+ type: {
18
+ type: 'string',
19
+ description: 'Filter by type: tools, extensions, runners, actors',
20
+ enum: %w[tools extensions runners actors]
21
+ },
22
+ refresh: {
23
+ type: 'boolean',
24
+ description: 'Force rebuild of the index (ignores cache)'
25
+ }
26
+ }
27
+ )
28
+
29
+ class << self
30
+ def call(extension: nil, type: nil, refresh: nil)
31
+ index = if refresh
32
+ StructuralIndex.save_cache(StructuralIndex.build)
33
+ else
34
+ StructuralIndex.load_or_build
35
+ end
36
+
37
+ result = StructuralIndex.filter(index, extension: extension, type: type)
38
+ text_response(result)
39
+ rescue StandardError => e
40
+ Legion::Logging.warn("StructuralIndexTool#call failed: #{e.message}") if defined?(Legion::Logging)
41
+ error_response("Failed: #{e.message}")
42
+ end
43
+
44
+ private
45
+
46
+ def text_response(data)
47
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
48
+ end
49
+
50
+ def error_response(msg)
51
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class ToolAudit < ::MCP::Tool
7
+ tool_name 'legion.tool_audit'
8
+ description 'Audit MCP tools for quality, categorization, and capability matrix. ' \
9
+ 'Returns issues, category assignments, and read/write capabilities.'
10
+
11
+ input_schema(
12
+ properties: {
13
+ mode: {
14
+ type: 'string',
15
+ description: 'Audit mode: summary (default), matrix (capability matrix), issues (quality issues only)',
16
+ enum: %w[summary matrix issues]
17
+ }
18
+ }
19
+ )
20
+
21
+ class << self
22
+ def call(mode: 'summary')
23
+ result = case mode
24
+ when 'matrix'
25
+ ToolQuality.capability_matrix
26
+ when 'issues'
27
+ ToolQuality.audit_all.select { |r| r[:quality] == :warn }
28
+ else
29
+ ToolQuality.summary
30
+ end
31
+
32
+ text_response(result)
33
+ rescue StandardError => e
34
+ Legion::Logging.warn("ToolAudit#call failed: #{e.message}") if defined?(Legion::Logging)
35
+ error_response("Failed: #{e.message}")
36
+ end
37
+
38
+ private
39
+
40
+ def text_response(data)
41
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
42
+ end
43
+
44
+ def error_response(msg)
45
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.6.6'
5
+ VERSION = '0.7.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.6.6
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -110,6 +110,7 @@ files:
110
110
  - ".gitignore"
111
111
  - ".rspec"
112
112
  - ".rubocop.yml"
113
+ - AGENTS.md
113
114
  - CHANGELOG.md
114
115
  - CLAUDE.md
115
116
  - CODEOWNERS
@@ -122,6 +123,7 @@ files:
122
123
  - lib/legion/mcp/actors/self_generate_cycle.rb
123
124
  - lib/legion/mcp/auth.rb
124
125
  - lib/legion/mcp/catalog_bridge.rb
126
+ - lib/legion/mcp/catalog_dispatcher.rb
125
127
  - lib/legion/mcp/client.rb
126
128
  - lib/legion/mcp/client/connection.rb
127
129
  - lib/legion/mcp/client/pool.rb
@@ -129,6 +131,8 @@ files:
129
131
  - lib/legion/mcp/cold_start.rb
130
132
  - lib/legion/mcp/context_compiler.rb
131
133
  - lib/legion/mcp/context_guard.rb
134
+ - lib/legion/mcp/deferred_registry.rb
135
+ - lib/legion/mcp/dynamic_injector.rb
132
136
  - lib/legion/mcp/embedding_index.rb
133
137
  - lib/legion/mcp/function_discovery.rb
134
138
  - lib/legion/mcp/gap_detector.rb
@@ -144,8 +148,11 @@ files:
144
148
  - lib/legion/mcp/self_generate.rb
145
149
  - lib/legion/mcp/server.rb
146
150
  - lib/legion/mcp/settings.rb
151
+ - lib/legion/mcp/state_tracker.rb
152
+ - lib/legion/mcp/structural_index.rb
147
153
  - lib/legion/mcp/tier_router.rb
148
154
  - lib/legion/mcp/tool_governance.rb
155
+ - lib/legion/mcp/tool_quality.rb
149
156
  - lib/legion/mcp/tools/absorb.rb
150
157
  - lib/legion/mcp/tools/ask_peer.rb
151
158
  - lib/legion/mcp/tools/broadcast_peers.rb
@@ -199,8 +206,12 @@ files:
199
206
  - lib/legion/mcp/tools/rbac_grants.rb
200
207
  - lib/legion/mcp/tools/routing_stats.rb
201
208
  - lib/legion/mcp/tools/run_task.rb
209
+ - lib/legion/mcp/tools/search_sessions.rb
202
210
  - lib/legion/mcp/tools/show_worker.rb
211
+ - lib/legion/mcp/tools/state_diff.rb
212
+ - lib/legion/mcp/tools/structural_index.rb
203
213
  - lib/legion/mcp/tools/team_summary.rb
214
+ - lib/legion/mcp/tools/tool_audit.rb
204
215
  - lib/legion/mcp/tools/update_chain.rb
205
216
  - lib/legion/mcp/tools/update_relationship.rb
206
217
  - lib/legion/mcp/tools/update_schedule.rb