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,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ListSchedules < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.list_schedules'
|
|
8
|
+
description 'List all schedules. Requires lex-scheduler.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
active: { type: 'boolean', description: 'Filter by active status' },
|
|
13
|
+
limit: { type: 'integer', description: 'Max results (default 25, max 100)' }
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(active: nil, limit: 25)
|
|
19
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
20
|
+
return error_response('lex-scheduler is not loaded') unless scheduler_loaded?
|
|
21
|
+
|
|
22
|
+
limit = limit.to_i.clamp(1, 100)
|
|
23
|
+
dataset = Legion::Extensions::Scheduler::Data::Model::Schedule.order(:id)
|
|
24
|
+
dataset = dataset.where(active: true) if active == true
|
|
25
|
+
text_response(dataset.limit(limit).all.map(&:values))
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
error_response("Failed to list schedules: #{e.message}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def data_connected?
|
|
33
|
+
Legion::Settings[:data][:connected]
|
|
34
|
+
rescue StandardError
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def scheduler_loaded? = defined?(Legion::Extensions::Scheduler)
|
|
39
|
+
|
|
40
|
+
def text_response(data)
|
|
41
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def error_response(msg)
|
|
45
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ListTasks < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.list_tasks'
|
|
8
|
+
description 'List recent tasks with optional filtering by status or function_id.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
status: { type: 'string', description: 'Filter by task status' },
|
|
13
|
+
function_id: { type: 'integer', description: 'Filter by function ID' },
|
|
14
|
+
limit: { type: 'integer', description: 'Max results (default 25, max 100)' }
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(status: nil, function_id: nil, limit: 25)
|
|
20
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
21
|
+
|
|
22
|
+
limit = limit.to_i.clamp(1, 100)
|
|
23
|
+
dataset = Legion::Data::Model::Task.order(Sequel.desc(:id))
|
|
24
|
+
dataset = dataset.where(status: status) if status
|
|
25
|
+
dataset = dataset.where(function_id: function_id.to_i) if function_id
|
|
26
|
+
text_response(dataset.limit(limit).all.map(&:values))
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
error_response("Failed to list tasks: #{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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ListWorkers < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.list_workers'
|
|
8
|
+
description 'List digital workers with optional filtering by team, owner, or lifecycle state.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
team: { type: 'string', description: 'Filter by team name' },
|
|
13
|
+
owner_msid: { type: 'string', description: 'Filter by owner MSID' },
|
|
14
|
+
lifecycle_state: { type: 'string',
|
|
15
|
+
description: 'Filter by lifecycle state (bootstrap, active, paused, retired, terminated)' },
|
|
16
|
+
limit: { type: 'integer', description: 'Max results (default 20, max 100)' }
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def call(team: nil, owner_msid: nil, lifecycle_state: nil, limit: 20)
|
|
22
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
23
|
+
|
|
24
|
+
limit = limit.to_i.clamp(1, 100)
|
|
25
|
+
dataset = Legion::Data::Model::DigitalWorker.order(Sequel.desc(:id))
|
|
26
|
+
dataset = dataset.where(team: team) if team
|
|
27
|
+
dataset = dataset.where(owner_msid: owner_msid) if owner_msid
|
|
28
|
+
dataset = dataset.where(lifecycle_state: lifecycle_state) if lifecycle_state
|
|
29
|
+
|
|
30
|
+
text_response(dataset.limit(limit).all.map(&:values))
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
error_response("Failed to list workers: #{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,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RbacAssignments < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.rbac_assignments'
|
|
8
|
+
description 'List RBAC role assignments. Filterable by team, role, or principal.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
team: { type: 'string', description: 'Filter by team' },
|
|
13
|
+
role: { type: 'string', description: 'Filter by role name' },
|
|
14
|
+
principal: { type: 'string', description: 'Filter by principal ID' }
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(team: nil, role: nil, principal: nil)
|
|
20
|
+
return error_response('legion-rbac not installed') unless defined?(Legion::Rbac)
|
|
21
|
+
return error_response('legion-data not connected') unless Legion::Rbac::Store.db_available?
|
|
22
|
+
|
|
23
|
+
ds = Legion::Data::Model::RbacRoleAssignment.dataset
|
|
24
|
+
ds = ds.where(team: team) if team
|
|
25
|
+
ds = ds.where(role: role) if role
|
|
26
|
+
ds = ds.where(principal_id: principal) if principal
|
|
27
|
+
text_response(ds.all.map(&:values))
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
error_response("Failed to list assignments: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RbacCheck < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.rbac_check'
|
|
8
|
+
description 'Dry-run authorization check. Evaluates RBAC policies without enforcing.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
principal: { type: 'string', description: 'Principal ID to check' },
|
|
13
|
+
action: { type: 'string', description: 'Action (read, execute, manage, etc.)' },
|
|
14
|
+
resource: { type: 'string', description: 'Resource path (e.g. runners/lex-github/*)' },
|
|
15
|
+
roles: { type: 'array', items: { type: 'string' }, description: 'Roles to evaluate' },
|
|
16
|
+
team: { type: 'string', description: 'Team scope' }
|
|
17
|
+
},
|
|
18
|
+
required: %w[principal action resource roles]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def call(principal:, action:, resource:, roles: [], team: nil)
|
|
23
|
+
return error_response('legion-rbac not installed') unless defined?(Legion::Rbac)
|
|
24
|
+
|
|
25
|
+
p = Legion::Rbac::Principal.new(id: principal, roles: roles, team: team)
|
|
26
|
+
result = Legion::Rbac::PolicyEngine.evaluate(principal: p, action: action, resource: resource,
|
|
27
|
+
enforce: false)
|
|
28
|
+
text_response(result)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
error_response("RBAC check failed: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
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,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RbacGrants < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.rbac_grants'
|
|
8
|
+
description 'List RBAC runner grants. Filterable by team.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
team: { type: 'string', description: 'Filter by team' }
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def call(team: nil)
|
|
18
|
+
return error_response('legion-rbac not installed') unless defined?(Legion::Rbac)
|
|
19
|
+
return error_response('legion-data not connected') unless Legion::Rbac::Store.db_available?
|
|
20
|
+
|
|
21
|
+
ds = Legion::Data::Model::RbacRunnerGrant.dataset
|
|
22
|
+
ds = ds.where(team: team) if team
|
|
23
|
+
text_response(ds.all.map(&:values))
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
error_response("Failed to list grants: #{e.message}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def text_response(data)
|
|
31
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def error_response(msg)
|
|
35
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RoutingStats < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.routing_stats'
|
|
8
|
+
description 'Retrieve LLM routing statistics: breakdown by provider, model, and routing reason. Requires lex-metering.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
worker_id: { type: 'string', description: 'Optional: filter stats to a specific worker UUID' }
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def call(worker_id: nil)
|
|
18
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
19
|
+
return error_response('lex-metering is not loaded') unless metering_available?
|
|
20
|
+
|
|
21
|
+
runner = Object.new.extend(Legion::Extensions::Metering::Runners::Metering)
|
|
22
|
+
stats = runner.routing_stats(worker_id: worker_id)
|
|
23
|
+
text_response(stats)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
error_response("Failed to fetch routing stats: #{e.message}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def data_connected?
|
|
31
|
+
Legion::Settings[:data][:connected]
|
|
32
|
+
rescue StandardError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def metering_available?
|
|
37
|
+
defined?(Legion::Extensions::Metering::Runners::Metering)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def text_response(data)
|
|
41
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def error_response(msg)
|
|
45
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class RunTask < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.run_task'
|
|
8
|
+
description 'Execute a Legion task using dot notation (e.g., "http.request.get"). Returns the task result.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
task: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Dot notation path: extension.runner.function (e.g., "http.request.get")'
|
|
15
|
+
},
|
|
16
|
+
params: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
description: 'Parameters to pass to the task function',
|
|
19
|
+
additionalProperties: true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
required: ['task']
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def call(task:, params: {})
|
|
27
|
+
parts = task.split('.')
|
|
28
|
+
return error_response("Invalid dot notation '#{task}'. Expected format: extension.runner.function") unless parts.length == 3
|
|
29
|
+
|
|
30
|
+
ext_name, runner_name, function_name = parts
|
|
31
|
+
runner_class = resolve_runner_class(ext_name, runner_name)
|
|
32
|
+
|
|
33
|
+
result = Legion::Ingress.run(
|
|
34
|
+
payload: params,
|
|
35
|
+
runner_class: runner_class,
|
|
36
|
+
function: function_name.to_sym,
|
|
37
|
+
source: 'mcp',
|
|
38
|
+
check_subtask: true,
|
|
39
|
+
generate_task: true
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
text_response(result)
|
|
43
|
+
rescue NameError => e
|
|
44
|
+
error_response("Runner not found: #{e.message}")
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
error_response("Task execution failed: #{e.message}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def resolve_runner_class(ext_name, runner_name)
|
|
52
|
+
ext_part = ext_name.split('_').map(&:capitalize).join
|
|
53
|
+
runner_part = runner_name.split('_').map(&:capitalize).join
|
|
54
|
+
"Legion::Extensions::#{ext_part}::Runners::#{runner_part}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def text_response(data)
|
|
58
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def error_response(message)
|
|
62
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: message }) }], error: true)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class ShowWorker < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.show_worker'
|
|
8
|
+
description 'Get full details for a single digital worker by ID.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
worker_id: { type: 'string', description: 'UUID of the digital worker' }
|
|
13
|
+
},
|
|
14
|
+
required: ['worker_id']
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(worker_id:)
|
|
19
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
20
|
+
|
|
21
|
+
worker = Legion::DigitalWorker.find(worker_id: worker_id)
|
|
22
|
+
return error_response("Worker not found: #{worker_id}") unless worker
|
|
23
|
+
|
|
24
|
+
text_response(worker.values)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
error_response("Failed to fetch worker: #{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,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class TeamSummary < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.team_summary'
|
|
8
|
+
description 'Get a summary of all digital workers for a team, including lifecycle state breakdown.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
team: { type: 'string', description: 'Team name to summarize' }
|
|
13
|
+
},
|
|
14
|
+
required: ['team']
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def call(team:)
|
|
19
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
20
|
+
|
|
21
|
+
workers = Legion::DigitalWorker.by_team(team: team).all
|
|
22
|
+
breakdown = workers.each_with_object(Hash.new(0)) { |w, counts| counts[w.values[:lifecycle_state]] += 1 }
|
|
23
|
+
|
|
24
|
+
text_response({
|
|
25
|
+
team: team,
|
|
26
|
+
total: workers.size,
|
|
27
|
+
lifecycle_states: breakdown,
|
|
28
|
+
workers: workers.map do |w|
|
|
29
|
+
w.values.slice(:worker_id, :name, :lifecycle_state, :owner_msid, :business_role)
|
|
30
|
+
end
|
|
31
|
+
})
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
error_response("Failed to fetch team summary: #{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 UpdateChain < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.update_chain'
|
|
8
|
+
description 'Update an existing task chain.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'integer', description: 'Chain ID' },
|
|
13
|
+
name: { type: 'string', description: 'New chain name' }
|
|
14
|
+
},
|
|
15
|
+
required: ['id']
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(id:, **attrs)
|
|
20
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
21
|
+
return error_response('chain data model is not available') unless chain_model?
|
|
22
|
+
|
|
23
|
+
record = Legion::Data::Model::Chain[id.to_i]
|
|
24
|
+
return error_response("Chain #{id} not found") unless record
|
|
25
|
+
|
|
26
|
+
record.update(attrs) unless attrs.empty?
|
|
27
|
+
record.refresh
|
|
28
|
+
text_response(record.values)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
error_response("Failed to update chain: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def data_connected?
|
|
36
|
+
Legion::Settings[:data][:connected]
|
|
37
|
+
rescue StandardError
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def chain_model? = Legion::Data::Model.const_defined?(:Chain)
|
|
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,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class UpdateRelationship < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.update_relationship'
|
|
8
|
+
description 'Update an existing relationship.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: 'integer', description: 'Relationship ID' },
|
|
13
|
+
trigger_function_id: { type: 'integer', description: 'New trigger function ID' },
|
|
14
|
+
target_function_id: { type: 'integer', description: 'New target function ID' }
|
|
15
|
+
},
|
|
16
|
+
required: ['id']
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def call(id:, **attrs)
|
|
21
|
+
return error_response('legion-data is not connected') unless data_connected?
|
|
22
|
+
return error_response('relationship data model is not available') unless relationship_model?
|
|
23
|
+
|
|
24
|
+
record = Legion::Data::Model::Relationship[id.to_i]
|
|
25
|
+
return error_response("Relationship #{id} not found") unless record
|
|
26
|
+
|
|
27
|
+
record.update(attrs) unless attrs.empty?
|
|
28
|
+
record.refresh
|
|
29
|
+
text_response(record.values)
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
error_response("Failed to update relationship: #{e.message}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def data_connected?
|
|
37
|
+
Legion::Settings[:data][:connected]
|
|
38
|
+
rescue StandardError
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def relationship_model? = Legion::Data::Model.const_defined?(:Relationship)
|
|
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
|