legion-mcp 0.1.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 +7 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +40 -0
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +101 -0
- data/Gemfile +10 -0
- data/LICENSE +167 -0
- data/README.md +182 -0
- data/Rakefile +5 -0
- data/legion-mcp.gemspec +35 -0
- data/lib/legion/mcp/auth.rb +50 -0
- data/lib/legion/mcp/context_compiler.rb +173 -0
- data/lib/legion/mcp/context_guard.rb +105 -0
- data/lib/legion/mcp/embedding_index.rb +113 -0
- data/lib/legion/mcp/observer.rb +171 -0
- data/lib/legion/mcp/pattern_store.rb +303 -0
- data/lib/legion/mcp/resources/extension_info.rb +67 -0
- data/lib/legion/mcp/resources/runner_catalog.rb +63 -0
- data/lib/legion/mcp/server.rb +178 -0
- data/lib/legion/mcp/tier_router.rb +122 -0
- data/lib/legion/mcp/tool_governance.rb +77 -0
- data/lib/legion/mcp/tools/create_chain.rb +50 -0
- data/lib/legion/mcp/tools/create_relationship.rb +51 -0
- data/lib/legion/mcp/tools/create_schedule.rb +64 -0
- data/lib/legion/mcp/tools/delete_chain.rb +52 -0
- data/lib/legion/mcp/tools/delete_relationship.rb +52 -0
- data/lib/legion/mcp/tools/delete_schedule.rb +52 -0
- data/lib/legion/mcp/tools/delete_task.rb +49 -0
- data/lib/legion/mcp/tools/describe_runner.rb +92 -0
- data/lib/legion/mcp/tools/disable_extension.rb +50 -0
- data/lib/legion/mcp/tools/discover_tools.rb +53 -0
- data/lib/legion/mcp/tools/do_action.rb +85 -0
- data/lib/legion/mcp/tools/enable_extension.rb +50 -0
- data/lib/legion/mcp/tools/get_config.rb +63 -0
- data/lib/legion/mcp/tools/get_extension.rb +56 -0
- data/lib/legion/mcp/tools/get_status.rb +50 -0
- data/lib/legion/mcp/tools/get_task.rb +48 -0
- data/lib/legion/mcp/tools/get_task_logs.rb +56 -0
- data/lib/legion/mcp/tools/list_chains.rb +48 -0
- data/lib/legion/mcp/tools/list_extensions.rb +46 -0
- data/lib/legion/mcp/tools/list_relationships.rb +45 -0
- data/lib/legion/mcp/tools/list_schedules.rb +51 -0
- data/lib/legion/mcp/tools/list_tasks.rb +50 -0
- data/lib/legion/mcp/tools/list_workers.rb +54 -0
- data/lib/legion/mcp/tools/rbac_assignments.rb +45 -0
- data/lib/legion/mcp/tools/rbac_check.rb +46 -0
- data/lib/legion/mcp/tools/rbac_grants.rb +41 -0
- data/lib/legion/mcp/tools/routing_stats.rb +51 -0
- data/lib/legion/mcp/tools/run_task.rb +68 -0
- data/lib/legion/mcp/tools/show_worker.rb +48 -0
- data/lib/legion/mcp/tools/team_summary.rb +55 -0
- data/lib/legion/mcp/tools/update_chain.rb +54 -0
- data/lib/legion/mcp/tools/update_relationship.rb +55 -0
- data/lib/legion/mcp/tools/update_schedule.rb +65 -0
- data/lib/legion/mcp/tools/worker_costs.rb +55 -0
- data/lib/legion/mcp/tools/worker_lifecycle.rb +54 -0
- data/lib/legion/mcp/usage_filter.rb +86 -0
- data/lib/legion/mcp/version.rb +7 -0
- data/lib/legion/mcp.rb +30 -0
- metadata +195 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class DisableExtension < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.disable_extension'
|
|
8
|
+
description 'Disable a Legion extension by ID.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'integer', description: 'Extension ID' }
|
|
13
|
+
},
|
|
14
|
+
required: ['id']
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(id:)
|
|
19
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
20
|
+
|
|
21
|
+
ext = Legion::Data::Model::Extension[id.to_i]
|
|
22
|
+
return error_response("Extension #{id} not found") unless ext
|
|
23
|
+
|
|
24
|
+
ext.update(active: false)
|
|
25
|
+
ext.refresh
|
|
26
|
+
text_response(ext.values)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
error_response("Failed to disable extension: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def data_connected?
|
|
34
|
+
Legion::Settings[:data][:connected]
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def text_response(data)
|
|
40
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error_response(msg)
|
|
44
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class DiscoverTools < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.tools'
|
|
8
|
+
description 'Discover available Legion tools by category or intent. Returns compressed definitions to reduce context.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
category: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Tool category: tasks, chains, relationships, extensions, schedules, workers, rbac, status, describe'
|
|
15
|
+
},
|
|
16
|
+
intent: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Describe what you want to do and relevant tools will be ranked'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
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)
|
|
30
|
+
elsif intent
|
|
31
|
+
results = ContextCompiler.match_tools(intent, limit: 5)
|
|
32
|
+
text_response({ matched_tools: results })
|
|
33
|
+
else
|
|
34
|
+
text_response(ContextCompiler.compressed_catalog)
|
|
35
|
+
end
|
|
36
|
+
rescue StandardError => e
|
|
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,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class DoAction < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.do'
|
|
8
|
+
description 'Execute a Legion action by describing what you want to do in natural language. ' \
|
|
9
|
+
'Routes to the best matching tool automatically. Learned patterns are served ' \
|
|
10
|
+
'instantly without LLM.'
|
|
11
|
+
|
|
12
|
+
input_schema(
|
|
13
|
+
properties: {
|
|
14
|
+
intent: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Natural language description (e.g., "list all running tasks")'
|
|
17
|
+
},
|
|
18
|
+
params: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
description: 'Parameters to pass to the matched tool',
|
|
21
|
+
additionalProperties: true
|
|
22
|
+
},
|
|
23
|
+
context: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
description: 'Additional context (service, environment, etc.)',
|
|
26
|
+
additionalProperties: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
required: ['intent']
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def call(intent:, params: {}, context: {})
|
|
34
|
+
# Try Tier 0 first (learned patterns)
|
|
35
|
+
tier_result = try_tier0(intent, params, context)
|
|
36
|
+
if tier_result && tier_result[:tier].zero?
|
|
37
|
+
return text_response(tier_result[:response].merge(
|
|
38
|
+
_meta: { tier: 0,
|
|
39
|
+
latency_ms: tier_result[:latency_ms],
|
|
40
|
+
confidence: tier_result[:pattern_confidence] }
|
|
41
|
+
))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Fall back to ContextCompiler tool matching (original behavior)
|
|
45
|
+
matched = ContextCompiler.match_tool(intent)
|
|
46
|
+
return error_response("No matching tool found for intent: #{intent}") if matched.nil?
|
|
47
|
+
|
|
48
|
+
Legion::MCP::Observer.record_intent(intent, matched) if defined?(Legion::MCP::Observer)
|
|
49
|
+
|
|
50
|
+
tool_params = params.transform_keys(&:to_sym)
|
|
51
|
+
if tool_params.empty?
|
|
52
|
+
matched.call
|
|
53
|
+
else
|
|
54
|
+
matched.call(**tool_params)
|
|
55
|
+
end
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
error_response("Failed: #{e.message}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def try_tier0(intent, params, context)
|
|
63
|
+
return nil unless defined?(Legion::MCP::TierRouter)
|
|
64
|
+
|
|
65
|
+
Legion::MCP::TierRouter.route(
|
|
66
|
+
intent: intent,
|
|
67
|
+
params: params.transform_keys(&:to_sym),
|
|
68
|
+
context: context.transform_keys(&:to_sym)
|
|
69
|
+
)
|
|
70
|
+
rescue StandardError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def text_response(data)
|
|
75
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def error_response(msg)
|
|
79
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class EnableExtension < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.enable_extension'
|
|
8
|
+
description 'Enable a Legion extension by ID.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'integer', description: 'Extension ID' }
|
|
13
|
+
},
|
|
14
|
+
required: ['id']
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(id:)
|
|
19
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
20
|
+
|
|
21
|
+
ext = Legion::Data::Model::Extension[id.to_i]
|
|
22
|
+
return error_response("Extension #{id} not found") unless ext
|
|
23
|
+
|
|
24
|
+
ext.update(active: true)
|
|
25
|
+
ext.refresh
|
|
26
|
+
text_response(ext.values)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
error_response("Failed to enable extension: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def data_connected?
|
|
34
|
+
Legion::Settings[:data][:connected]
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def text_response(data)
|
|
40
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error_response(msg)
|
|
44
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class GetConfig < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.get_config'
|
|
8
|
+
description 'Get Legion configuration (sensitive values are redacted).'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
section: { type: 'string', description: 'Specific config section (e.g., "transport", "data")' }
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
SENSITIVE_KEYS = %i[password secret token key cert private_key api_key].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(section: nil)
|
|
20
|
+
settings = Legion::Settings.loader.to_hash
|
|
21
|
+
|
|
22
|
+
if section
|
|
23
|
+
key = section.to_sym
|
|
24
|
+
return error_response("Setting '#{section}' not found") unless settings.key?(key)
|
|
25
|
+
|
|
26
|
+
value = settings[key]
|
|
27
|
+
value = redact_hash(value) if value.is_a?(Hash)
|
|
28
|
+
text_response({ key: key, value: value })
|
|
29
|
+
else
|
|
30
|
+
text_response(redact_hash(settings))
|
|
31
|
+
end
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
error_response("Failed to get config: #{e.message}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def redact_hash(hash)
|
|
39
|
+
return hash unless hash.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
hash.each_with_object({}) do |(k, v), result|
|
|
42
|
+
result[k] = if v.is_a?(Hash)
|
|
43
|
+
redact_hash(v)
|
|
44
|
+
elsif SENSITIVE_KEYS.any? { |s| k.to_s.include?(s.to_s) }
|
|
45
|
+
'[REDACTED]'
|
|
46
|
+
else
|
|
47
|
+
v
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def text_response(data)
|
|
53
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def error_response(msg)
|
|
57
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class GetExtension < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.get_extension'
|
|
8
|
+
description 'Get detailed info about an extension including its runners and functions.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'integer', description: 'Extension ID' }
|
|
13
|
+
},
|
|
14
|
+
required: ['id']
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(id:)
|
|
19
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
20
|
+
|
|
21
|
+
ext = Legion::Data::Model::Extension[id.to_i]
|
|
22
|
+
return error_response("Extension #{id} not found") unless ext
|
|
23
|
+
|
|
24
|
+
runners = Legion::Data::Model::Runner.where(extension_id: id.to_i).all
|
|
25
|
+
result = ext.values.merge(
|
|
26
|
+
runners: runners.map do |r|
|
|
27
|
+
functions = Legion::Data::Model::Function.where(runner_id: r.values[:id]).all
|
|
28
|
+
r.values.merge(functions: functions.map(&:values))
|
|
29
|
+
end
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
text_response(result)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
error_response("Failed to get extension: #{e.message}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def data_connected?
|
|
40
|
+
Legion::Settings[:data][:connected]
|
|
41
|
+
rescue StandardError
|
|
42
|
+
false
|
|
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,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class GetStatus < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.get_status'
|
|
8
|
+
description 'Get Legion service health status and component info.'
|
|
9
|
+
|
|
10
|
+
input_schema(properties: {})
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def call
|
|
14
|
+
status = {
|
|
15
|
+
version: Legion::VERSION,
|
|
16
|
+
ready: begin
|
|
17
|
+
Legion::Readiness.ready?
|
|
18
|
+
rescue StandardError
|
|
19
|
+
false
|
|
20
|
+
end,
|
|
21
|
+
components: begin
|
|
22
|
+
Legion::Readiness.to_h
|
|
23
|
+
rescue StandardError
|
|
24
|
+
{}
|
|
25
|
+
end,
|
|
26
|
+
node: begin
|
|
27
|
+
Legion::Settings[:client][:name]
|
|
28
|
+
rescue StandardError
|
|
29
|
+
'unknown'
|
|
30
|
+
end
|
|
31
|
+
}
|
|
32
|
+
text_response(status)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
error_response("Failed to get status: #{e.message}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def text_response(data)
|
|
40
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error_response(msg)
|
|
44
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class GetTask < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.get_task'
|
|
8
|
+
description 'Get details of a specific task by ID.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'integer', description: 'Task ID' }
|
|
13
|
+
},
|
|
14
|
+
required: ['id']
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(id:)
|
|
19
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
20
|
+
|
|
21
|
+
task = Legion::Data::Model::Task[id.to_i]
|
|
22
|
+
return error_response("Task #{id} not found") unless task
|
|
23
|
+
|
|
24
|
+
text_response(task.values)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
error_response("Failed to get task: #{e.message}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def data_connected?
|
|
32
|
+
Legion::Settings[:data][:connected]
|
|
33
|
+
rescue StandardError
|
|
34
|
+
false
|
|
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,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class GetTaskLogs < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.get_task_logs'
|
|
8
|
+
description 'Get execution logs for a specific task.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'integer', description: 'Task ID' },
|
|
13
|
+
limit: { type: 'integer', description: 'Max log entries (default 50)' }
|
|
14
|
+
},
|
|
15
|
+
required: ['id']
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(id:, limit: 50)
|
|
20
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
21
|
+
|
|
22
|
+
task = Legion::Data::Model::Task[id.to_i]
|
|
23
|
+
return error_response("Task #{id} not found") unless task
|
|
24
|
+
|
|
25
|
+
limit = limit.to_i.clamp(1, 100)
|
|
26
|
+
logs = Legion::Data::Model::TaskLog
|
|
27
|
+
.where(task_id: id.to_i)
|
|
28
|
+
.order(Sequel.desc(:id))
|
|
29
|
+
.limit(limit)
|
|
30
|
+
.all.map(&:values)
|
|
31
|
+
|
|
32
|
+
text_response(logs)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
error_response("Failed to get task logs: #{e.message}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def data_connected?
|
|
40
|
+
Legion::Settings[:data][:connected]
|
|
41
|
+
rescue StandardError
|
|
42
|
+
false
|
|
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,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ListChains < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.list_chains'
|
|
8
|
+
description 'List all task chains.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
limit: { type: 'integer', description: 'Max results (default 25, max 100)' }
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def call(limit: 25)
|
|
18
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
19
|
+
return error_response('chain data model is not available') unless chain_model?
|
|
20
|
+
|
|
21
|
+
limit = limit.to_i.clamp(1, 100)
|
|
22
|
+
text_response(Legion::Data::Model::Chain.order(:id).limit(limit).all.map(&:values))
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
error_response("Failed to list chains: #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def data_connected?
|
|
30
|
+
Legion::Settings[:data][:connected]
|
|
31
|
+
rescue StandardError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def chain_model? = Legion::Data::Model.const_defined?(:Chain)
|
|
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,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ListExtensions < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.list_extensions'
|
|
8
|
+
description 'List all installed Legion extensions with status.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
active: { type: 'boolean', description: 'Filter by active status' }
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def call(active: nil)
|
|
18
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
19
|
+
|
|
20
|
+
dataset = Legion::Data::Model::Extension.order(:id)
|
|
21
|
+
dataset = dataset.where(active: true) if active == true
|
|
22
|
+
text_response(dataset.all.map(&:values))
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
error_response("Failed to list extensions: #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def data_connected?
|
|
30
|
+
Legion::Settings[:data][:connected]
|
|
31
|
+
rescue StandardError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def text_response(data)
|
|
36
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def error_response(msg)
|
|
40
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ListRelationships < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.list_relationships'
|
|
8
|
+
description 'List all task relationships.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
limit: { type: 'integer', description: 'Max results (default 25, max 100)' }
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def call(limit: 25)
|
|
18
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
19
|
+
|
|
20
|
+
limit = limit.to_i.clamp(1, 100)
|
|
21
|
+
text_response(Legion::Data::Model::Relationship.order(:id).limit(limit).all.map(&:values))
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
error_response("Failed to list relationships: #{e.message}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def data_connected?
|
|
29
|
+
Legion::Settings[:data][:connected]
|
|
30
|
+
rescue StandardError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def text_response(data)
|
|
35
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def error_response(msg)
|
|
39
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|