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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class UpdateSchedule < ::MCP::Tool
7
+ tool_name 'legion.update_schedule'
8
+ description 'Update an existing schedule.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ id: { type: 'integer', description: 'Schedule ID' },
13
+ cron: { type: 'string', description: 'New cron expression' },
14
+ interval: { type: 'integer', description: 'New interval in seconds' },
15
+ active: { type: 'boolean', description: 'Active status' },
16
+ function_id: { type: 'integer', description: 'New function ID' },
17
+ payload: { type: 'object', description: 'New payload', additionalProperties: true }
18
+ },
19
+ required: ['id']
20
+ )
21
+
22
+ class << self
23
+ def call(id:, **attrs)
24
+ return error_response('legion-data is not connected') unless data_connected?
25
+ return error_response('lex-scheduler is not loaded') unless scheduler_loaded?
26
+
27
+ record = Legion::Extensions::Scheduler::Data::Model::Schedule[id.to_i]
28
+ return error_response("Schedule #{id} not found") unless record
29
+
30
+ updates = {}
31
+ updates[:cron] = attrs[:cron] if attrs.key?(:cron)
32
+ updates[:interval] = attrs[:interval].to_i if attrs.key?(:interval)
33
+ updates[:active] = attrs[:active] if attrs.key?(:active)
34
+ updates[:function_id] = attrs[:function_id].to_i if attrs.key?(:function_id)
35
+ updates[:payload] = Legion::JSON.dump(attrs[:payload]) if attrs.key?(:payload)
36
+
37
+ record.update(updates) unless updates.empty?
38
+ record.refresh
39
+ text_response(record.values)
40
+ rescue StandardError => e
41
+ error_response("Failed to update schedule: #{e.message}")
42
+ end
43
+
44
+ private
45
+
46
+ def data_connected?
47
+ Legion::Settings[:data][:connected]
48
+ rescue StandardError
49
+ false
50
+ end
51
+
52
+ def scheduler_loaded? = defined?(Legion::Extensions::Scheduler)
53
+
54
+ def text_response(data)
55
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
56
+ end
57
+
58
+ def error_response(msg)
59
+ ::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class WorkerCosts < ::MCP::Tool
7
+ tool_name 'legion.worker_costs'
8
+ description 'Retrieve cost data for a digital worker. Returns a stub response until lex-metering is available.'
9
+
10
+ input_schema(
11
+ properties: {
12
+ worker_id: { type: 'string', description: 'UUID of the digital worker' },
13
+ period: { type: 'string', description: 'Reporting period: daily, weekly, monthly (default: weekly)' }
14
+ },
15
+ required: ['worker_id']
16
+ )
17
+
18
+ class << self
19
+ def call(worker_id:, period: 'weekly')
20
+ return error_response('legion-data is not connected') unless data_connected?
21
+
22
+ worker = Legion::DigitalWorker.find(worker_id: worker_id)
23
+ return error_response("Worker not found: #{worker_id}") unless worker
24
+
25
+ text_response({
26
+ worker_id: worker_id,
27
+ period: period,
28
+ available: false,
29
+ message: 'Cost metering is not yet available. Install lex-metering to enable worker cost tracking.',
30
+ worker_name: worker.values[:name]
31
+ })
32
+ rescue StandardError => e
33
+ error_response("Failed to fetch worker costs: #{e.message}")
34
+ end
35
+
36
+ private
37
+
38
+ def data_connected?
39
+ Legion::Settings[:data][:connected]
40
+ rescue StandardError
41
+ false
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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Tools
6
+ class WorkerLifecycle < ::MCP::Tool
7
+ tool_name 'legion.worker_lifecycle'
8
+ description 'Transition a digital worker to a new lifecycle state (bootstrap, active, paused, retired, terminated).'
9
+
10
+ input_schema(
11
+ properties: {
12
+ worker_id: { type: 'string', description: 'UUID of the digital worker' },
13
+ to_state: { type: 'string', description: 'Target lifecycle state' },
14
+ by: { type: 'string', description: 'MSID or identifier of the person performing the transition' },
15
+ reason: { type: 'string', description: 'Optional reason for the transition' }
16
+ },
17
+ required: %w[worker_id to_state by]
18
+ )
19
+
20
+ class << self
21
+ def call(worker_id:, to_state:, by:, reason: nil)
22
+ return error_response('legion-data is not connected') unless data_connected?
23
+
24
+ worker = Legion::DigitalWorker.find(worker_id: worker_id)
25
+ return error_response("Worker not found: #{worker_id}") unless worker
26
+
27
+ updated = Legion::DigitalWorker::Lifecycle.transition!(worker, to_state: to_state, by: by, reason: reason)
28
+ text_response(updated.values)
29
+ rescue Legion::DigitalWorker::Lifecycle::InvalidTransition => e
30
+ error_response("Invalid transition: #{e.message}")
31
+ rescue StandardError => e
32
+ error_response("Lifecycle transition failed: #{e.message}")
33
+ end
34
+
35
+ private
36
+
37
+ def data_connected?
38
+ Legion::Settings[:data][:connected]
39
+ rescue StandardError
40
+ false
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,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module UsageFilter
6
+ ESSENTIAL_TOOLS = %w[
7
+ legion.do legion.tools legion.run_task legion.get_status legion.describe_runner
8
+ ].freeze
9
+
10
+ FREQUENCY_WEIGHT = 0.5
11
+ RECENCY_WEIGHT = 0.3
12
+ KEYWORD_WEIGHT = 0.2
13
+ BASELINE_SCORE = 0.1
14
+
15
+ module_function
16
+
17
+ def score_tools(tool_names, keywords: [])
18
+ all_stats = Observer.all_tool_stats
19
+ call_counts = tool_names.map { |n| all_stats.dig(n, :call_count) || 0 }
20
+ max_calls = call_counts.max || 0
21
+
22
+ tool_names.each_with_object({}) do |name, hash|
23
+ stats = all_stats[name]
24
+
25
+ freq_score = if max_calls.positive? && stats
26
+ (stats[:call_count].to_f / max_calls) * FREQUENCY_WEIGHT
27
+ else
28
+ 0.0
29
+ end
30
+
31
+ rec_score = if stats&.dig(:last_used)
32
+ recency_decay(stats[:last_used]) * RECENCY_WEIGHT
33
+ else
34
+ 0.0
35
+ end
36
+
37
+ kw_score = keyword_match(name, keywords) * KEYWORD_WEIGHT
38
+
39
+ total = freq_score + rec_score + kw_score
40
+ total = BASELINE_SCORE if total.zero?
41
+
42
+ hash[name] = total.round(6)
43
+ end
44
+ end
45
+
46
+ def ranked_tools(tool_names, limit: nil, keywords: [])
47
+ scores = score_tools(tool_names, keywords: keywords)
48
+ ranked = tool_names.sort_by { |n| -scores.fetch(n, BASELINE_SCORE) }
49
+ limit ? ranked.first(limit) : ranked
50
+ end
51
+
52
+ def prune_dead_tools(tool_names, prune_after_seconds: 86_400 * 30)
53
+ stats = Observer.stats
54
+ window = stats[:since]
55
+ elapsed = window ? (Time.now - window) : 0
56
+
57
+ return tool_names if elapsed < prune_after_seconds
58
+
59
+ all_stats = Observer.all_tool_stats
60
+ tool_names.reject do |name|
61
+ next false if ESSENTIAL_TOOLS.include?(name)
62
+
63
+ calls = all_stats.dig(name, :call_count) || 0
64
+ calls.zero?
65
+ end
66
+ end
67
+
68
+ def recency_decay(last_used)
69
+ return 0.0 unless last_used
70
+
71
+ age_seconds = Time.now - last_used
72
+ return 1.0 if age_seconds <= 0
73
+
74
+ decay = 1.0 - (age_seconds / 86_400.0)
75
+ decay.clamp(0.0, 1.0)
76
+ end
77
+
78
+ def keyword_match(tool_name, keywords)
79
+ return 0.0 if keywords.nil? || keywords.empty?
80
+
81
+ hits = keywords.count { |kw| tool_name.include?(kw.to_s) }
82
+ hits.to_f / keywords.size
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
data/lib/legion/mcp.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+ require 'legion/json'
5
+ require_relative 'mcp/version'
6
+
7
+ require_relative 'mcp/auth'
8
+ require_relative 'mcp/tool_governance'
9
+ require_relative 'mcp/server'
10
+
11
+ module Legion
12
+ module MCP
13
+ class << self
14
+ def server
15
+ @server ||= Server.build
16
+ end
17
+
18
+ def server_for(token:)
19
+ auth_result = Auth.authenticate(token)
20
+ return { error: auth_result[:error] } unless auth_result[:authenticated]
21
+
22
+ Server.build(identity: auth_result[:identity])
23
+ end
24
+
25
+ def reset!
26
+ @server = nil
27
+ end
28
+ end
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: legion-mcp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mcp
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.8'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.8'
40
+ - !ruby/object:Gem::Dependency
41
+ name: legion-data
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.4'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.4'
54
+ - !ruby/object:Gem::Dependency
55
+ name: legion-json
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1.2'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '1.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: legion-logging
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0.3'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0.3'
82
+ - !ruby/object:Gem::Dependency
83
+ name: legion-settings
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0.3'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0.3'
96
+ description: Model Context Protocol server with semantic tool matching, observation
97
+ pipeline, and tiered inference for LegionIO
98
+ email:
99
+ - matthewdiverson@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files:
103
+ - CHANGELOG.md
104
+ - LICENSE
105
+ - README.md
106
+ files:
107
+ - ".github/workflows/ci.yml"
108
+ - ".gitignore"
109
+ - ".rspec"
110
+ - ".rubocop.yml"
111
+ - CHANGELOG.md
112
+ - CLAUDE.md
113
+ - Gemfile
114
+ - LICENSE
115
+ - README.md
116
+ - Rakefile
117
+ - legion-mcp.gemspec
118
+ - lib/legion/mcp.rb
119
+ - lib/legion/mcp/auth.rb
120
+ - lib/legion/mcp/context_compiler.rb
121
+ - lib/legion/mcp/context_guard.rb
122
+ - lib/legion/mcp/embedding_index.rb
123
+ - lib/legion/mcp/observer.rb
124
+ - lib/legion/mcp/pattern_store.rb
125
+ - lib/legion/mcp/resources/extension_info.rb
126
+ - lib/legion/mcp/resources/runner_catalog.rb
127
+ - lib/legion/mcp/server.rb
128
+ - lib/legion/mcp/tier_router.rb
129
+ - lib/legion/mcp/tool_governance.rb
130
+ - lib/legion/mcp/tools/create_chain.rb
131
+ - lib/legion/mcp/tools/create_relationship.rb
132
+ - lib/legion/mcp/tools/create_schedule.rb
133
+ - lib/legion/mcp/tools/delete_chain.rb
134
+ - lib/legion/mcp/tools/delete_relationship.rb
135
+ - lib/legion/mcp/tools/delete_schedule.rb
136
+ - lib/legion/mcp/tools/delete_task.rb
137
+ - lib/legion/mcp/tools/describe_runner.rb
138
+ - lib/legion/mcp/tools/disable_extension.rb
139
+ - lib/legion/mcp/tools/discover_tools.rb
140
+ - lib/legion/mcp/tools/do_action.rb
141
+ - lib/legion/mcp/tools/enable_extension.rb
142
+ - lib/legion/mcp/tools/get_config.rb
143
+ - lib/legion/mcp/tools/get_extension.rb
144
+ - lib/legion/mcp/tools/get_status.rb
145
+ - lib/legion/mcp/tools/get_task.rb
146
+ - lib/legion/mcp/tools/get_task_logs.rb
147
+ - lib/legion/mcp/tools/list_chains.rb
148
+ - lib/legion/mcp/tools/list_extensions.rb
149
+ - lib/legion/mcp/tools/list_relationships.rb
150
+ - lib/legion/mcp/tools/list_schedules.rb
151
+ - lib/legion/mcp/tools/list_tasks.rb
152
+ - lib/legion/mcp/tools/list_workers.rb
153
+ - lib/legion/mcp/tools/rbac_assignments.rb
154
+ - lib/legion/mcp/tools/rbac_check.rb
155
+ - lib/legion/mcp/tools/rbac_grants.rb
156
+ - lib/legion/mcp/tools/routing_stats.rb
157
+ - lib/legion/mcp/tools/run_task.rb
158
+ - lib/legion/mcp/tools/show_worker.rb
159
+ - lib/legion/mcp/tools/team_summary.rb
160
+ - lib/legion/mcp/tools/update_chain.rb
161
+ - lib/legion/mcp/tools/update_relationship.rb
162
+ - lib/legion/mcp/tools/update_schedule.rb
163
+ - lib/legion/mcp/tools/worker_costs.rb
164
+ - lib/legion/mcp/tools/worker_lifecycle.rb
165
+ - lib/legion/mcp/usage_filter.rb
166
+ - lib/legion/mcp/version.rb
167
+ homepage: https://github.com/LegionIO/legion-mcp
168
+ licenses:
169
+ - Apache-2.0
170
+ metadata:
171
+ bug_tracker_uri: https://github.com/LegionIO/legion-mcp/issues
172
+ changelog_uri: https://github.com/LegionIO/legion-mcp/blob/main/CHANGELOG.md
173
+ documentation_uri: https://github.com/LegionIO/legion-mcp
174
+ homepage_uri: https://github.com/LegionIO/LegionIO
175
+ source_code_uri: https://github.com/LegionIO/legion-mcp
176
+ wiki_uri: https://github.com/LegionIO/legion-mcp/wiki
177
+ rubygems_mfa_required: 'true'
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '3.4'
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubygems_version: 3.6.9
193
+ specification_version: 4
194
+ summary: MCP server for the LegionIO framework
195
+ test_files: []