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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +16 -0
  3. data/.gitignore +6 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +40 -0
  6. data/CHANGELOG.md +20 -0
  7. data/CLAUDE.md +101 -0
  8. data/Gemfile +10 -0
  9. data/LICENSE +167 -0
  10. data/README.md +182 -0
  11. data/Rakefile +5 -0
  12. data/legion-mcp.gemspec +35 -0
  13. data/lib/legion/mcp/auth.rb +50 -0
  14. data/lib/legion/mcp/context_compiler.rb +173 -0
  15. data/lib/legion/mcp/context_guard.rb +105 -0
  16. data/lib/legion/mcp/embedding_index.rb +113 -0
  17. data/lib/legion/mcp/observer.rb +171 -0
  18. data/lib/legion/mcp/pattern_store.rb +303 -0
  19. data/lib/legion/mcp/resources/extension_info.rb +67 -0
  20. data/lib/legion/mcp/resources/runner_catalog.rb +63 -0
  21. data/lib/legion/mcp/server.rb +178 -0
  22. data/lib/legion/mcp/tier_router.rb +122 -0
  23. data/lib/legion/mcp/tool_governance.rb +77 -0
  24. data/lib/legion/mcp/tools/create_chain.rb +50 -0
  25. data/lib/legion/mcp/tools/create_relationship.rb +51 -0
  26. data/lib/legion/mcp/tools/create_schedule.rb +64 -0
  27. data/lib/legion/mcp/tools/delete_chain.rb +52 -0
  28. data/lib/legion/mcp/tools/delete_relationship.rb +52 -0
  29. data/lib/legion/mcp/tools/delete_schedule.rb +52 -0
  30. data/lib/legion/mcp/tools/delete_task.rb +49 -0
  31. data/lib/legion/mcp/tools/describe_runner.rb +92 -0
  32. data/lib/legion/mcp/tools/disable_extension.rb +50 -0
  33. data/lib/legion/mcp/tools/discover_tools.rb +53 -0
  34. data/lib/legion/mcp/tools/do_action.rb +85 -0
  35. data/lib/legion/mcp/tools/enable_extension.rb +50 -0
  36. data/lib/legion/mcp/tools/get_config.rb +63 -0
  37. data/lib/legion/mcp/tools/get_extension.rb +56 -0
  38. data/lib/legion/mcp/tools/get_status.rb +50 -0
  39. data/lib/legion/mcp/tools/get_task.rb +48 -0
  40. data/lib/legion/mcp/tools/get_task_logs.rb +56 -0
  41. data/lib/legion/mcp/tools/list_chains.rb +48 -0
  42. data/lib/legion/mcp/tools/list_extensions.rb +46 -0
  43. data/lib/legion/mcp/tools/list_relationships.rb +45 -0
  44. data/lib/legion/mcp/tools/list_schedules.rb +51 -0
  45. data/lib/legion/mcp/tools/list_tasks.rb +50 -0
  46. data/lib/legion/mcp/tools/list_workers.rb +54 -0
  47. data/lib/legion/mcp/tools/rbac_assignments.rb +45 -0
  48. data/lib/legion/mcp/tools/rbac_check.rb +46 -0
  49. data/lib/legion/mcp/tools/rbac_grants.rb +41 -0
  50. data/lib/legion/mcp/tools/routing_stats.rb +51 -0
  51. data/lib/legion/mcp/tools/run_task.rb +68 -0
  52. data/lib/legion/mcp/tools/show_worker.rb +48 -0
  53. data/lib/legion/mcp/tools/team_summary.rb +55 -0
  54. data/lib/legion/mcp/tools/update_chain.rb +54 -0
  55. data/lib/legion/mcp/tools/update_relationship.rb +55 -0
  56. data/lib/legion/mcp/tools/update_schedule.rb +65 -0
  57. data/lib/legion/mcp/tools/worker_costs.rb +55 -0
  58. data/lib/legion/mcp/tools/worker_lifecycle.rb +54 -0
  59. data/lib/legion/mcp/usage_filter.rb +86 -0
  60. data/lib/legion/mcp/version.rb +7 -0
  61. data/lib/legion/mcp.rb +30 -0
  62. 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