legion-mcp 0.4.0 → 0.4.2

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: bc7341c88afa89639cebd05ccffc1dfb00792062373fe295743f525c9c0380c9
4
- data.tar.gz: d2b832be2e2942286489edad666215e13f8de19b888a32551bdedef4e70b1f7f
3
+ metadata.gz: 423fca2b66220aa4bfd5dce33490a6fb92826da7e999a21be9afed00610fd435
4
+ data.tar.gz: 7e9a664b742ee174140f2463c008bd99475736afe074c056cf298d9742a7ec5d
5
5
  SHA512:
6
- metadata.gz: 89874ef7825b398392ed7966573379a9c9edc55500cb874bb79897b3b1f65631ff1c594c90420948468d00d9cbe9e8a83838d482d491d7260cea76450b977358
7
- data.tar.gz: 58b8e23f9a5a37a87f2d50150cdc8872deaa654524c1ce92a507ac7b157aa9b687a6b99a591d3841f94e37a567dc7f30a4922ca55e5e1646072cf84331947354
6
+ metadata.gz: d610777e7f105a5b72e4a58fb0a06cd129361639837638b49888de9b6feeb3697a5147184d5d415ba95315d2bafcf3011b9a6c5d7515b8ceec3155a8173477a9
7
+ data.tar.gz: 6c74faf7a062f61e8b9e10d79b6236dc9b361d6b5ff657bfe651bfa731f6a633a8c8acaf0a68f907b49cd333c4328d77e919d45a114c8e8b7a40323bbfeb9dac
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # legion-mcp Changelog
2
2
 
3
+ ## [0.4.2] - 2026-03-22
4
+
5
+ ### Added
6
+ - `legion.ask_peer` — synchronous RPC query to a specific mesh peer via `lex-mesh` `request_task`
7
+ - `legion.list_peers` — list all registered mesh agents, with optional capability filter via `find_agents`
8
+ - `legion.notify_peer` — fire-and-forget unicast notification to a specific mesh agent via `send_message`
9
+ - `legion.broadcast_peers` — broadcast to all agents or multicast to a capability group via `send_message`
10
+ - `legion.mesh_status` — retrieve current mesh network state via `mesh_status`
11
+ - All 5 tools registered in `TOOL_CLASSES`; total tool count raised from 45 to 50
12
+ - Specs for all 5 new tools (25 examples)
13
+ - Updated `server_spec.rb` tool count assertion from 45 to 50
14
+
15
+ ## [0.4.1] - 2026-03-19
16
+
17
+ ### Added
18
+ - `legion.prompt_list` — list all stored prompt templates via lex-prompt Client
19
+ - `legion.prompt_show` — fetch a prompt by name, version, or tag via lex-prompt Client
20
+ - `legion.prompt_run` — render a prompt template with ERB variable substitution via lex-prompt Client
21
+ - `legion.dataset_list` — list all stored datasets via lex-dataset Client
22
+ - `legion.dataset_show` — fetch a dataset with all rows, optionally version-pinned, via lex-dataset Client
23
+ - `legion.experiment_results` — retrieve per-row results and summary for a named experiment from lex-dataset
24
+ - `legion.eval_list` — list available evaluator templates via lex-eval Client
25
+ - `legion.eval_run` — run a single input/output pair through a named evaluator via lex-eval Client
26
+ - `legion.eval_results` — retrieve stored experiment results via lex-dataset experiment store
27
+ - All 9 tools registered in `TOOL_CLASSES`; total tool count raised from 36 to 45
28
+ - Specs for all 9 new tools
29
+
3
30
  ## [0.4.0] - 2026-03-20
4
31
 
5
32
  ### Added
data/CODEOWNERS ADDED
@@ -0,0 +1,39 @@
1
+ # Default owner — all files
2
+ * @Esity
3
+
4
+ # Core library code
5
+ # lib/ @Esity @future-ai-team
6
+
7
+ # MCP tools (35 tool subclasses)
8
+ # lib/legion/mcp/tools/ @Esity @future-ai-team
9
+
10
+ # MCP resources
11
+ # lib/legion/mcp/resources/ @Esity @future-ai-team
12
+
13
+ # Tiered Behavioral Intelligence (TBI) — pattern store, tier router, context guard
14
+ # lib/legion/mcp/pattern_store.rb @Esity @future-ai-team
15
+ # lib/legion/mcp/tier_router.rb @Esity @future-ai-team
16
+ # lib/legion/mcp/context_guard.rb @Esity @future-ai-team
17
+
18
+ # Semantic matching and embeddings
19
+ # lib/legion/mcp/context_compiler.rb @Esity @future-ai-team
20
+ # lib/legion/mcp/embedding_index.rb @Esity @future-ai-team
21
+
22
+ # Observer pipeline and usage filter
23
+ # lib/legion/mcp/observer.rb @Esity @future-ai-team
24
+ # lib/legion/mcp/usage_filter.rb @Esity @future-ai-team
25
+
26
+ # Tool governance
27
+ # lib/legion/mcp/tool_governance.rb @Esity @future-security-team
28
+
29
+ # Authentication
30
+ # lib/legion/mcp/auth.rb @Esity @future-security-team
31
+
32
+ # Specs
33
+ # spec/ @Esity @future-contributors
34
+
35
+ # Documentation
36
+ # *.md @Esity @future-docs-team
37
+
38
+ # CI/CD
39
+ # .github/ @Esity
@@ -35,12 +35,26 @@ require_relative 'tools/routing_stats'
35
35
  require_relative 'tools/rbac_check'
36
36
  require_relative 'tools/rbac_assignments'
37
37
  require_relative 'tools/rbac_grants'
38
+ require_relative 'tools/prompt_list'
39
+ require_relative 'tools/prompt_show'
40
+ require_relative 'tools/prompt_run'
41
+ require_relative 'tools/dataset_list'
42
+ require_relative 'tools/dataset_show'
43
+ require_relative 'tools/experiment_results'
44
+ require_relative 'tools/eval_list'
45
+ require_relative 'tools/eval_run'
46
+ require_relative 'tools/eval_results'
38
47
  require_relative 'context_compiler'
39
48
  require_relative 'embedding_index'
40
49
  require_relative 'cold_start'
41
50
  require_relative 'tools/do_action'
42
51
  require_relative 'tools/plan_action'
43
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'
44
58
  require_relative 'resources/runner_catalog'
45
59
  require_relative 'resources/extension_info'
46
60
 
@@ -81,9 +95,23 @@ module Legion
81
95
  Tools::RbacCheck,
82
96
  Tools::RbacAssignments,
83
97
  Tools::RbacGrants,
98
+ Tools::PromptList,
99
+ Tools::PromptShow,
100
+ Tools::PromptRun,
101
+ Tools::DatasetList,
102
+ Tools::DatasetShow,
103
+ Tools::ExperimentResults,
104
+ Tools::EvalList,
105
+ Tools::EvalRun,
106
+ Tools::EvalResults,
84
107
  Tools::DoAction,
85
108
  Tools::PlanAction,
86
- Tools::DiscoverTools
109
+ Tools::DiscoverTools,
110
+ Tools::AskPeer,
111
+ Tools::ListPeers,
112
+ Tools::NotifyPeer,
113
+ Tools::BroadcastPeers,
114
+ Tools::MeshStatus
87
115
  ].freeze
88
116
 
89
117
  class << self
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class AskPeer < ::MCP::Tool
7
+ tool_name 'legion.ask_peer'
8
+ description 'Send a synchronous query to a specific mesh peer and return the result.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ to: { type: 'string', description: 'Agent ID or capability name to route to' },
13
+ query: { type: 'string', description: 'The question or request to send to the peer' },
14
+ timeout: { type: 'integer', description: 'Seconds to wait for response (default 30)' }
15
+ },
16
+ required: %w[to query]
17
+ )
18
+
19
+ class << self
20
+ def call(to:, query:, timeout: 30)
21
+ return error_response('lex-mesh is not available') unless mesh_available?
22
+
23
+ result = mesh_client.request_task(
24
+ from: 'legion.mcp',
25
+ to: to,
26
+ task: 'query',
27
+ payload: { query: query },
28
+ timeout: timeout
29
+ )
30
+ text_response(result)
31
+ rescue StandardError => e
32
+ error_response("Failed to query peer: #{e.message}")
33
+ end
34
+
35
+ private
36
+
37
+ def mesh_available?
38
+ defined?(Legion::Extensions::Mesh::Client)
39
+ end
40
+
41
+ def mesh_client
42
+ @mesh_client ||= Legion::Extensions::Mesh::Client.new
43
+ end
44
+
45
+ def text_response(data)
46
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
47
+ end
48
+
49
+ def error_response(msg)
50
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class BroadcastPeers < ::MCP::Tool
7
+ tool_name 'legion.broadcast_peers'
8
+ description 'Broadcast a message to all mesh agents, or multicast to agents with a specific capability.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ message: { type: 'string', description: 'Message content to broadcast' },
13
+ capability: { type: 'string', description: 'If provided, multicast only to agents with this capability' }
14
+ },
15
+ required: %w[message]
16
+ )
17
+
18
+ class << self
19
+ def call(message:, capability: nil)
20
+ return error_response('lex-mesh is not available') unless mesh_available?
21
+
22
+ pattern = capability ? :multicast : :broadcast
23
+ result = mesh_client.send_message(
24
+ from: 'legion.mcp',
25
+ to: capability || :all,
26
+ pattern: pattern,
27
+ payload: { message: message }
28
+ )
29
+ text_response(result)
30
+ rescue StandardError => e
31
+ error_response("Failed to broadcast: #{e.message}")
32
+ end
33
+
34
+ private
35
+
36
+ def mesh_available?
37
+ defined?(Legion::Extensions::Mesh::Client)
38
+ end
39
+
40
+ def mesh_client
41
+ @mesh_client ||= Legion::Extensions::Mesh::Client.new
42
+ end
43
+
44
+ def text_response(data)
45
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
46
+ end
47
+
48
+ def error_response(msg)
49
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class DatasetList < ::MCP::Tool
7
+ tool_name 'legion.dataset_list'
8
+ description 'List all stored datasets with their latest version and row counts.'
9
+
10
+ input_schema(properties: {})
11
+
12
+ class << self
13
+ def call
14
+ return error_response('lex-dataset is not loaded') unless extension_loaded?('dataset')
15
+
16
+ require 'legion/extensions/dataset/client'
17
+ client = Legion::Extensions::Dataset::Client.new(db: db)
18
+ result = client.list_datasets
19
+ text_response(result)
20
+ rescue StandardError => e
21
+ error_response("Failed to list datasets: #{e.message}")
22
+ end
23
+
24
+ private
25
+
26
+ def extension_loaded?(name)
27
+ require "legion/extensions/#{name}"
28
+ true
29
+ rescue LoadError
30
+ false
31
+ end
32
+
33
+ def db
34
+ Legion::Data.db
35
+ end
36
+
37
+ def text_response(data)
38
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
39
+ end
40
+
41
+ def error_response(msg)
42
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class DatasetShow < ::MCP::Tool
7
+ tool_name 'legion.dataset_show'
8
+ description 'Retrieve a dataset by name including all rows, optionally pinned to a specific version.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ name: { type: 'string', description: 'Name of the dataset' },
13
+ version: { type: 'integer', description: 'Specific version to fetch (default: latest)' }
14
+ },
15
+ required: ['name']
16
+ )
17
+
18
+ class << self
19
+ def call(name:, version: nil)
20
+ return error_response('lex-dataset is not loaded') unless extension_loaded?('dataset')
21
+
22
+ require 'legion/extensions/dataset/client'
23
+ client = Legion::Extensions::Dataset::Client.new(db: db)
24
+ result = client.get_dataset(name: name, version: version)
25
+ text_response(result)
26
+ rescue StandardError => e
27
+ error_response("Failed to fetch dataset: #{e.message}")
28
+ end
29
+
30
+ private
31
+
32
+ def extension_loaded?(name)
33
+ require "legion/extensions/#{name}"
34
+ true
35
+ rescue LoadError
36
+ false
37
+ end
38
+
39
+ def db
40
+ Legion::Data.db
41
+ end
42
+
43
+ def text_response(data)
44
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
45
+ end
46
+
47
+ def error_response(msg)
48
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class EvalList < ::MCP::Tool
7
+ tool_name 'legion.eval_list'
8
+ description 'List all available evaluator templates (LLM-as-judge and code-based).'
9
+
10
+ input_schema(properties: {})
11
+
12
+ class << self
13
+ def call
14
+ return error_response('lex-eval is not loaded') unless extension_loaded?('eval')
15
+
16
+ require 'legion/extensions/eval/client'
17
+ client = Legion::Extensions::Eval::Client.new(db: db)
18
+ result = client.list_evaluators
19
+ text_response(result)
20
+ rescue StandardError => e
21
+ error_response("Failed to list evaluators: #{e.message}")
22
+ end
23
+
24
+ private
25
+
26
+ def extension_loaded?(name)
27
+ require "legion/extensions/#{name}"
28
+ true
29
+ rescue LoadError
30
+ false
31
+ end
32
+
33
+ def db
34
+ Legion::Data.db
35
+ end
36
+
37
+ def text_response(data)
38
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
39
+ end
40
+
41
+ def error_response(msg)
42
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class EvalResults < ::MCP::Tool
7
+ tool_name 'legion.eval_results'
8
+ description 'Retrieve stored results for a named experiment from the dataset experiment store.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ experiment_name: { type: 'string', description: 'Name of the experiment to retrieve results for' }
13
+ },
14
+ required: ['experiment_name']
15
+ )
16
+
17
+ class << self
18
+ def call(experiment_name:)
19
+ return error_response('lex-dataset is not loaded') unless extension_loaded?('dataset')
20
+
21
+ require 'legion/extensions/dataset/client'
22
+ client = Legion::Extensions::Dataset::Client.new(db: db)
23
+ result = fetch_experiment(client, experiment_name)
24
+ text_response(result)
25
+ rescue StandardError => e
26
+ error_response("Failed to fetch eval results: #{e.message}")
27
+ end
28
+
29
+ private
30
+
31
+ def fetch_experiment(client, name)
32
+ db_handle = client.instance_variable_get(:@db)
33
+ return { error: 'database_unavailable' } unless db_handle
34
+
35
+ exp = db_handle[:experiments].where(name: name).first
36
+ return { error: 'not_found' } unless exp
37
+
38
+ rows = db_handle[:experiment_results]
39
+ .where(experiment_id: exp[:id])
40
+ .order(:row_index)
41
+ .all
42
+ .map { |r| { row_index: r[:row_index], passed: r[:passed], latency_ms: r[:latency_ms] } }
43
+
44
+ summary = begin
45
+ ::JSON.parse(exp[:summary], symbolize_names: true)
46
+ rescue StandardError
47
+ {}
48
+ end
49
+
50
+ { experiment_id: exp[:id], name: exp[:name], status: exp[:status],
51
+ created_at: exp[:created_at], completed_at: exp[:completed_at],
52
+ summary: summary, rows: rows }
53
+ end
54
+
55
+ def extension_loaded?(name)
56
+ require "legion/extensions/#{name}"
57
+ true
58
+ rescue LoadError
59
+ false
60
+ end
61
+
62
+ def db
63
+ Legion::Data.db
64
+ end
65
+
66
+ def text_response(data)
67
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
68
+ end
69
+
70
+ def error_response(msg)
71
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class EvalRun < ::MCP::Tool
7
+ tool_name 'legion.eval_run'
8
+ description 'Run an evaluator against a single input/output pair and return pass/fail with score.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ evaluator_name: { type: 'string', description: 'Name of the evaluator template to use' },
13
+ input: { type: 'string', description: 'The original input/prompt given to the model' },
14
+ output: { type: 'string', description: 'The model output to evaluate' },
15
+ expected: { type: 'string', description: 'Optional expected/reference output for comparison' }
16
+ },
17
+ required: %w[evaluator_name input output]
18
+ )
19
+
20
+ class << self
21
+ def call(evaluator_name:, input:, output:, expected: nil)
22
+ return error_response('lex-eval is not loaded') unless extension_loaded?('eval')
23
+
24
+ require 'legion/extensions/eval/client'
25
+ client = Legion::Extensions::Eval::Client.new(db: db)
26
+ inputs = [{ input: input, output: output, expected: expected }.compact]
27
+ result = client.run_evaluation(evaluator_name: evaluator_name, inputs: inputs)
28
+ text_response(result)
29
+ rescue StandardError => e
30
+ error_response("Failed to run evaluation: #{e.message}")
31
+ end
32
+
33
+ private
34
+
35
+ def extension_loaded?(name)
36
+ require "legion/extensions/#{name}"
37
+ true
38
+ rescue LoadError
39
+ false
40
+ end
41
+
42
+ def db
43
+ Legion::Data.db
44
+ end
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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class ExperimentResults < ::MCP::Tool
7
+ tool_name 'legion.experiment_results'
8
+ description 'Retrieve stored results for a named experiment, including per-row pass/fail and summary stats.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ name: { type: 'string', description: 'Name of the experiment to retrieve results for' }
13
+ },
14
+ required: ['name']
15
+ )
16
+
17
+ class << self
18
+ def call(name:)
19
+ return error_response('lex-dataset is not loaded') unless extension_loaded?('dataset')
20
+
21
+ require 'legion/extensions/dataset/client'
22
+ client = Legion::Extensions::Dataset::Client.new(db: db)
23
+ result = fetch_experiment(client, name)
24
+ text_response(result)
25
+ rescue StandardError => e
26
+ error_response("Failed to fetch experiment results: #{e.message}")
27
+ end
28
+
29
+ private
30
+
31
+ def fetch_experiment(client, name)
32
+ db_handle = client.instance_variable_get(:@db)
33
+ return { error: 'database_unavailable' } unless db_handle
34
+
35
+ exp = db_handle[:experiments].where(name: name).first
36
+ return { error: 'not_found' } unless exp
37
+
38
+ rows = db_handle[:experiment_results]
39
+ .where(experiment_id: exp[:id])
40
+ .order(:row_index)
41
+ .all
42
+ .map { |r| { row_index: r[:row_index], passed: r[:passed], latency_ms: r[:latency_ms] } }
43
+
44
+ summary = begin
45
+ ::JSON.parse(exp[:summary], symbolize_names: true)
46
+ rescue StandardError
47
+ {}
48
+ end
49
+
50
+ { experiment_id: exp[:id], name: exp[:name], status: exp[:status],
51
+ created_at: exp[:created_at], completed_at: exp[:completed_at],
52
+ summary: summary, rows: rows }
53
+ end
54
+
55
+ def extension_loaded?(name)
56
+ require "legion/extensions/#{name}"
57
+ true
58
+ rescue LoadError
59
+ false
60
+ end
61
+
62
+ def db
63
+ Legion::Data.db
64
+ end
65
+
66
+ def text_response(data)
67
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
68
+ end
69
+
70
+ def error_response(msg)
71
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class ListPeers < ::MCP::Tool
7
+ tool_name 'legion.list_peers'
8
+ description 'List all registered mesh agents, optionally filtered by capability.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ capability: { type: 'string', description: 'Filter agents by capability' }
13
+ }
14
+ )
15
+
16
+ class << self
17
+ def call(capability: nil)
18
+ return error_response('lex-mesh is not available') unless mesh_available?
19
+
20
+ result = mesh_client.find_agents(capability: capability)
21
+ text_response(result)
22
+ rescue StandardError => e
23
+ error_response("Failed to list peers: #{e.message}")
24
+ end
25
+
26
+ private
27
+
28
+ def mesh_available?
29
+ defined?(Legion::Extensions::Mesh::Client)
30
+ end
31
+
32
+ def mesh_client
33
+ @mesh_client ||= Legion::Extensions::Mesh::Client.new
34
+ end
35
+
36
+ def text_response(data)
37
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
38
+ end
39
+
40
+ def error_response(msg)
41
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class MeshStatus < ::MCP::Tool
7
+ tool_name 'legion.mesh_status'
8
+ description 'Get current mesh network status including registered agents and topology.'
9
+
10
+ input_schema(properties: {})
11
+
12
+ class << self
13
+ def call
14
+ return error_response('lex-mesh is not available') unless mesh_available?
15
+
16
+ result = mesh_client.mesh_status
17
+ text_response(result)
18
+ rescue StandardError => e
19
+ error_response("Failed to get mesh status: #{e.message}")
20
+ end
21
+
22
+ private
23
+
24
+ def mesh_available?
25
+ defined?(Legion::Extensions::Mesh::Client)
26
+ end
27
+
28
+ def mesh_client
29
+ @mesh_client ||= Legion::Extensions::Mesh::Client.new
30
+ end
31
+
32
+ def text_response(data)
33
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
34
+ end
35
+
36
+ def error_response(msg)
37
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class NotifyPeer < ::MCP::Tool
7
+ tool_name 'legion.notify_peer'
8
+ description 'Send a fire-and-forget async notification to a specific mesh agent.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ to: { type: 'string', description: 'Target agent ID' },
13
+ message: { type: 'string', description: 'Notification content to send' }
14
+ },
15
+ required: %w[to message]
16
+ )
17
+
18
+ class << self
19
+ def call(to:, message:)
20
+ return error_response('lex-mesh is not available') unless mesh_available?
21
+
22
+ result = mesh_client.send_message(
23
+ from: 'legion.mcp',
24
+ to: to,
25
+ pattern: :unicast,
26
+ payload: { message: message }
27
+ )
28
+ text_response(result)
29
+ rescue StandardError => e
30
+ error_response("Failed to notify peer: #{e.message}")
31
+ end
32
+
33
+ private
34
+
35
+ def mesh_available?
36
+ defined?(Legion::Extensions::Mesh::Client)
37
+ end
38
+
39
+ def mesh_client
40
+ @mesh_client ||= Legion::Extensions::Mesh::Client.new
41
+ end
42
+
43
+ def text_response(data)
44
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
45
+ end
46
+
47
+ def error_response(msg)
48
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class PromptList < ::MCP::Tool
7
+ tool_name 'legion.prompt_list'
8
+ description 'List all stored LLM prompt templates with their latest version and metadata.'
9
+
10
+ input_schema(properties: {})
11
+
12
+ class << self
13
+ def call
14
+ return error_response('lex-prompt is not loaded') unless extension_loaded?('prompt')
15
+
16
+ require 'legion/extensions/prompt/client'
17
+ client = Legion::Extensions::Prompt::Client.new(db: db)
18
+ result = client.list_prompts
19
+ text_response(result)
20
+ rescue StandardError => e
21
+ error_response("Failed to list prompts: #{e.message}")
22
+ end
23
+
24
+ private
25
+
26
+ def extension_loaded?(name)
27
+ require "legion/extensions/#{name}"
28
+ true
29
+ rescue LoadError
30
+ false
31
+ end
32
+
33
+ def db
34
+ Legion::Data.db
35
+ end
36
+
37
+ def text_response(data)
38
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
39
+ end
40
+
41
+ def error_response(msg)
42
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class PromptRun < ::MCP::Tool
7
+ tool_name 'legion.prompt_run'
8
+ description 'Render a prompt template with variable substitution and return the final text.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ name: { type: 'string', description: 'Name of the prompt template to render' },
13
+ variables: {
14
+ type: 'object',
15
+ description: 'Key/value pairs to substitute into the ERB template',
16
+ additionalProperties: true
17
+ },
18
+ version: { type: 'integer', description: 'Specific version to render (default: latest)' }
19
+ },
20
+ required: ['name']
21
+ )
22
+
23
+ class << self
24
+ def call(name:, variables: {}, version: nil)
25
+ return error_response('lex-prompt is not loaded') unless extension_loaded?('prompt')
26
+
27
+ require 'legion/extensions/prompt/client'
28
+ client = Legion::Extensions::Prompt::Client.new(db: db)
29
+ result = client.render_prompt(name: name, variables: variables, version: version)
30
+ text_response(result)
31
+ rescue StandardError => e
32
+ error_response("Failed to render prompt: #{e.message}")
33
+ end
34
+
35
+ private
36
+
37
+ def extension_loaded?(name)
38
+ require "legion/extensions/#{name}"
39
+ true
40
+ rescue LoadError
41
+ false
42
+ end
43
+
44
+ def db
45
+ Legion::Data.db
46
+ end
47
+
48
+ def text_response(data)
49
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
50
+ end
51
+
52
+ def error_response(msg)
53
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class PromptShow < ::MCP::Tool
7
+ tool_name 'legion.prompt_show'
8
+ description 'Retrieve a prompt template by name, optionally pinned to a specific version or tag.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ name: { type: 'string', description: 'Name of the prompt template' },
13
+ version: { type: 'integer', description: 'Specific version number to fetch (default: latest)' },
14
+ tag: { type: 'string', description: 'Named tag to resolve (e.g. "stable", "production")' }
15
+ },
16
+ required: ['name']
17
+ )
18
+
19
+ class << self
20
+ def call(name:, version: nil, tag: nil)
21
+ return error_response('lex-prompt is not loaded') unless extension_loaded?('prompt')
22
+
23
+ require 'legion/extensions/prompt/client'
24
+ client = Legion::Extensions::Prompt::Client.new(db: db)
25
+ result = client.get_prompt(name: name, version: version, tag: tag)
26
+ text_response(result)
27
+ rescue StandardError => e
28
+ error_response("Failed to fetch prompt: #{e.message}")
29
+ end
30
+
31
+ private
32
+
33
+ def extension_loaded?(name)
34
+ require "legion/extensions/#{name}"
35
+ true
36
+ rescue LoadError
37
+ false
38
+ end
39
+
40
+ def db
41
+ Legion::Data.db
42
+ end
43
+
44
+ def text_response(data)
45
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
46
+ end
47
+
48
+ def error_response(msg)
49
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.4.0'
5
+ VERSION = '0.4.2'
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.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -110,6 +110,7 @@ files:
110
110
  - ".rubocop.yml"
111
111
  - CHANGELOG.md
112
112
  - CLAUDE.md
113
+ - CODEOWNERS
113
114
  - Gemfile
114
115
  - LICENSE
115
116
  - README.md
@@ -134,9 +135,13 @@ files:
134
135
  - lib/legion/mcp/server.rb
135
136
  - lib/legion/mcp/tier_router.rb
136
137
  - lib/legion/mcp/tool_governance.rb
138
+ - lib/legion/mcp/tools/ask_peer.rb
139
+ - lib/legion/mcp/tools/broadcast_peers.rb
137
140
  - lib/legion/mcp/tools/create_chain.rb
138
141
  - lib/legion/mcp/tools/create_relationship.rb
139
142
  - lib/legion/mcp/tools/create_schedule.rb
143
+ - lib/legion/mcp/tools/dataset_list.rb
144
+ - lib/legion/mcp/tools/dataset_show.rb
140
145
  - lib/legion/mcp/tools/delete_chain.rb
141
146
  - lib/legion/mcp/tools/delete_relationship.rb
142
147
  - lib/legion/mcp/tools/delete_schedule.rb
@@ -146,6 +151,10 @@ files:
146
151
  - lib/legion/mcp/tools/discover_tools.rb
147
152
  - lib/legion/mcp/tools/do_action.rb
148
153
  - lib/legion/mcp/tools/enable_extension.rb
154
+ - lib/legion/mcp/tools/eval_list.rb
155
+ - lib/legion/mcp/tools/eval_results.rb
156
+ - lib/legion/mcp/tools/eval_run.rb
157
+ - lib/legion/mcp/tools/experiment_results.rb
149
158
  - lib/legion/mcp/tools/get_config.rb
150
159
  - lib/legion/mcp/tools/get_extension.rb
151
160
  - lib/legion/mcp/tools/get_status.rb
@@ -153,11 +162,17 @@ files:
153
162
  - lib/legion/mcp/tools/get_task_logs.rb
154
163
  - lib/legion/mcp/tools/list_chains.rb
155
164
  - lib/legion/mcp/tools/list_extensions.rb
165
+ - lib/legion/mcp/tools/list_peers.rb
156
166
  - lib/legion/mcp/tools/list_relationships.rb
157
167
  - lib/legion/mcp/tools/list_schedules.rb
158
168
  - lib/legion/mcp/tools/list_tasks.rb
159
169
  - lib/legion/mcp/tools/list_workers.rb
170
+ - lib/legion/mcp/tools/mesh_status.rb
171
+ - lib/legion/mcp/tools/notify_peer.rb
160
172
  - lib/legion/mcp/tools/plan_action.rb
173
+ - lib/legion/mcp/tools/prompt_list.rb
174
+ - lib/legion/mcp/tools/prompt_run.rb
175
+ - lib/legion/mcp/tools/prompt_show.rb
161
176
  - lib/legion/mcp/tools/rbac_assignments.rb
162
177
  - lib/legion/mcp/tools/rbac_check.rb
163
178
  - lib/legion/mcp/tools/rbac_grants.rb