sidekiq-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.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ module Mcp
8
+ module Tools
9
+ class QueueHealthTool < Tool
10
+ description "Heuristics on whether queues are backed up or healthy"
11
+
12
+ arguments do
13
+ optional(:latency_threshold).filled(:integer).value(gteq?: 1)
14
+ optional(:size_threshold).filled(:integer).value(gteq?: 1)
15
+ end
16
+
17
+ def perform(latency_threshold: 60, size_threshold: 100)
18
+ queues_health = Sidekiq::Queue.all.map do |queue|
19
+ latency = queue.latency
20
+ size = queue.size
21
+
22
+ # Determine health status
23
+ health_issues = []
24
+ health_issues << "High latency (#{latency.round(2)}s)" if latency > latency_threshold
25
+ health_issues << "Large queue size (#{size} jobs)" if size > size_threshold
26
+
27
+ health_status = health_issues.empty? ? "healthy" : "warning"
28
+ health_status = "critical" if health_issues.size > 1
29
+
30
+ {
31
+ name: queue.name,
32
+ size: size,
33
+ latency: latency.round(2),
34
+ health_status: health_status,
35
+ issues: health_issues,
36
+ recommendations: generate_recommendations(health_issues, size, latency)
37
+ }
38
+ end
39
+
40
+ overall_health = queues_health.any? { |q| q[:health_status] == "critical" } ? "critical" :
41
+ queues_health.any? { |q| q[:health_status] == "warning" } ? "warning" : "healthy"
42
+
43
+ {
44
+ overall_health: overall_health,
45
+ thresholds: {
46
+ latency_threshold: latency_threshold,
47
+ size_threshold: size_threshold
48
+ },
49
+ queues: queues_health
50
+ }.to_json
51
+ end
52
+
53
+ private
54
+
55
+ def generate_recommendations(issues, size, latency)
56
+ recommendations = []
57
+
58
+ if issues.any? { |i| i.include?("latency") }
59
+ recommendations << "Consider adding more workers or optimizing job performance"
60
+ end
61
+
62
+ if issues.any? { |i| i.include?("size") }
63
+ recommendations << "Queue is backing up - check for worker issues or increase concurrency"
64
+ end
65
+
66
+ if size > 1000
67
+ recommendations << "Very large queue - consider splitting into smaller queues or adding priority"
68
+ end
69
+
70
+ if latency > 300
71
+ recommendations << "Extremely high latency - immediate attention required"
72
+ end
73
+
74
+ recommendations
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ module Mcp
8
+ module Tools
9
+ class RemoveJobTool < Tool
10
+ description "Remove a job from any set (queue/schedule/retry/dead) by JID"
11
+
12
+ arguments do
13
+ required(:jid).filled(:string)
14
+ end
15
+
16
+ def perform(jid:)
17
+ # Try to find and remove from retry set
18
+ retry_set = Sidekiq::RetrySet.new
19
+ if (job = retry_set.find_job(jid))
20
+ job.delete
21
+ return "Job #{jid} removed from retry set"
22
+ end
23
+
24
+ # Try to find and remove from scheduled set
25
+ scheduled_set = Sidekiq::ScheduledSet.new
26
+ if (job = scheduled_set.find_job(jid))
27
+ job.delete
28
+ return "Job #{jid} removed from scheduled set"
29
+ end
30
+
31
+ # Try to find and remove from dead set
32
+ dead_set = Sidekiq::DeadSet.new
33
+ if (job = dead_set.find_job(jid))
34
+ job.delete
35
+ return "Job #{jid} removed from dead set"
36
+ end
37
+
38
+ # Try to find and remove from queues
39
+ Sidekiq::Queue.all.each do |queue|
40
+ if (job = queue.find_job(jid))
41
+ job.delete
42
+ return "Job #{jid} removed from queue '#{queue.name}'"
43
+ end
44
+ end
45
+
46
+ "Job #{jid} not found in any queue or set"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ module Mcp
8
+ module Tools
9
+ class RescheduleJobTool < Tool
10
+ description "Reschedule a job in the scheduled set to a new time"
11
+
12
+ arguments do
13
+ required(:jid).filled(:string)
14
+ required(:new_time).filled(:string) # ISO 8601 format
15
+ end
16
+
17
+ def perform(jid:, new_time:)
18
+ begin
19
+ new_timestamp = Time.parse(new_time)
20
+ rescue ArgumentError
21
+ return "Invalid time format. Please use ISO 8601 format (e.g., '2024-12-25T10:00:00Z')"
22
+ end
23
+
24
+ scheduled_set = Sidekiq::ScheduledSet.new
25
+ job = scheduled_set.find_job(jid)
26
+
27
+ if job
28
+ old_time = job.at
29
+ job.reschedule(new_timestamp)
30
+ "Job #{jid} rescheduled from #{old_time} to #{new_timestamp}"
31
+ else
32
+ "Job #{jid} not found in scheduled set"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ module Mcp
8
+ module Tools
9
+ class RetryJobTool < Tool
10
+ description "Retry a specific failed job by its JID"
11
+
12
+ arguments do
13
+ required(:jid).filled(:string)
14
+ end
15
+
16
+ def perform(jid:)
17
+ retry_set = Sidekiq::RetrySet.new
18
+ job = retry_set.find_job(jid)
19
+
20
+ if job
21
+ job.retry
22
+ "Job #{jid} has been retried successfully"
23
+ else
24
+ "Job #{jid} not found in retry set"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ module Mcp
8
+ module Tools
9
+ class SidekiqStatsTool < Tool
10
+ description "Get general Sidekiq statistics including processed, failed, busy, enqueued counts"
11
+
12
+ arguments do
13
+ # No arguments needed for general stats
14
+ end
15
+
16
+ def perform
17
+ stats = Sidekiq::Stats.new
18
+ {
19
+ processed: stats.processed,
20
+ failed: stats.failed,
21
+ busy: stats.workers_size,
22
+ enqueued: stats.enqueued,
23
+ scheduled: stats.scheduled_size,
24
+ retry: stats.retry_size,
25
+ dead: stats.dead_size,
26
+ default_queue_latency: stats.default_queue_latency
27
+ }.to_json
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ module Mcp
8
+ module Tools
9
+ class StreamStatsTool < Tool
10
+ description "Start streaming real-time Sidekiq statistics (use with SSE)"
11
+
12
+ arguments do
13
+ optional(:interval).filled(:integer).value(gteq?: 1, lteq?: 300) # 1-300 seconds
14
+ end
15
+
16
+ def perform(interval: 5)
17
+ # This tool is designed to work with SSE streaming
18
+ # In a real implementation, this would start a background task
19
+ # For now, we'll return the current stats with streaming info
20
+
21
+ stats = Sidekiq::Stats.new
22
+ current_stats = {
23
+ timestamp: Time.now.utc.iso8601,
24
+ processed: stats.processed,
25
+ failed: stats.failed,
26
+ busy: stats.workers_size,
27
+ enqueued: stats.enqueued,
28
+ scheduled: stats.scheduled_size,
29
+ retry: stats.retry_size,
30
+ dead: stats.dead_size,
31
+ processes: stats.processes_size,
32
+ default_queue_latency: stats.default_queue_latency
33
+ }
34
+
35
+ {
36
+ message: "Stats streaming initiated with #{interval}s interval",
37
+ current_stats: current_stats,
38
+ sse_endpoint: "/sidekiq-mcp/sse",
39
+ note: "Connect to SSE endpoint to receive real-time updates"
40
+ }.to_json
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../tool"
4
+ require "sidekiq/api"
5
+
6
+ module Sidekiq
7
+ module Mcp
8
+ module Tools
9
+ class WorkersCountTool < Tool
10
+ description "Show how many workers are running and busy"
11
+
12
+ arguments do
13
+ # No arguments needed
14
+ end
15
+
16
+ def perform
17
+ processes = Sidekiq::ProcessSet.new
18
+
19
+ total_workers = 0
20
+ busy_workers = 0
21
+ processes_info = []
22
+
23
+ processes.each do |process|
24
+ process_workers = process["concurrency"] || 0
25
+ process_busy = process["busy"] || 0
26
+
27
+ total_workers += process_workers
28
+ busy_workers += process_busy
29
+
30
+ processes_info << {
31
+ hostname: process["hostname"],
32
+ pid: process["pid"],
33
+ concurrency: process_workers,
34
+ busy: process_busy,
35
+ utilization: process_workers > 0 ? ((process_busy.to_f / process_workers) * 100).round(2) : 0,
36
+ started_at: process["started_at"]
37
+ }
38
+ end
39
+
40
+ {
41
+ total_processes: processes.size,
42
+ total_workers: total_workers,
43
+ busy_workers: busy_workers,
44
+ idle_workers: total_workers - busy_workers,
45
+ overall_utilization: total_workers > 0 ? ((busy_workers.to_f / total_workers) * 100).round(2) : 0,
46
+ processes: processes_info
47
+ }.to_json
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Mcp
5
+ module Tools
6
+ end
7
+
8
+ class ToolsRegistry
9
+ def initialize
10
+ register_all_tools
11
+ end
12
+
13
+ def list
14
+ @tools.values
15
+ end
16
+
17
+ def call(tool_name, arguments)
18
+ tool_class = @tool_classes[tool_name]
19
+ return "Unknown tool: #{tool_name}" unless tool_class
20
+
21
+ begin
22
+ tool_instance = tool_class.new
23
+ tool_instance.call(**arguments.transform_keys(&:to_sym))
24
+ rescue => e
25
+ "Error executing tool #{tool_name}: #{e.message}"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def register_all_tools
32
+ @tools = {}
33
+ @tool_classes = {}
34
+
35
+ # Auto-discover all tool classes
36
+ tool_classes = [
37
+ Tools::SidekiqStatsTool,
38
+ Tools::QueueDetailsTool,
39
+ Tools::RetryJobTool,
40
+ Tools::FailedJobsTool,
41
+ Tools::JobClassStatsTool,
42
+ Tools::JobDetailsTool,
43
+ Tools::ListScheduledJobsTool,
44
+ Tools::ListRetryJobsTool,
45
+ Tools::DeadJobsTool,
46
+ Tools::WorkersCountTool,
47
+ Tools::QueueHealthTool,
48
+ Tools::RemoveJobTool,
49
+ Tools::ClearQueueTool,
50
+ Tools::RescheduleJobTool,
51
+ Tools::KillJobTool,
52
+ Tools::ProcessSetTool,
53
+ Tools::StreamStatsTool
54
+ ]
55
+
56
+ # Also register original legacy tools
57
+ register_legacy_tools
58
+
59
+ tool_classes.each do |tool_class|
60
+ tool_def = tool_class.to_tool_definition
61
+ @tools[tool_def[:name]] = tool_def
62
+ @tool_classes[tool_def[:name]] = tool_class
63
+ end
64
+ end
65
+
66
+ def register_legacy_tools
67
+ # List queues tool
68
+ @tools["list_queues"] = {
69
+ name: "list_queues",
70
+ description: "List all Sidekiq queues with their sizes and latency information",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {},
74
+ required: []
75
+ }
76
+ }
77
+ @tool_classes["list_queues"] = Class.new(Tool) do
78
+ def perform
79
+ queues = Sidekiq::Queue.all.map do |queue|
80
+ {
81
+ name: queue.name,
82
+ size: queue.size,
83
+ latency: queue.latency
84
+ }
85
+ end
86
+ queues.to_json
87
+ end
88
+ end
89
+
90
+ # Busy workers tool
91
+ @tools["busy_workers"] = {
92
+ name: "busy_workers",
93
+ description: "List currently busy workers and their job details",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {},
97
+ required: []
98
+ }
99
+ }
100
+ @tool_classes["busy_workers"] = Class.new(Tool) do
101
+ def perform
102
+ workers = Sidekiq::Workers.new.map do |process_id, thread_id, work|
103
+ {
104
+ process_id: process_id,
105
+ thread_id: thread_id,
106
+ queue: work["queue"],
107
+ class: work["payload"]["class"],
108
+ args: work["payload"]["args"],
109
+ jid: work["payload"]["jid"],
110
+ run_at: work["run_at"]
111
+ }
112
+ end
113
+ workers.to_json
114
+ end
115
+ end
116
+
117
+ # Delete failed job tool
118
+ @tools["delete_failed_job"] = {
119
+ name: "delete_failed_job",
120
+ description: "Delete a specific failed job by its JID",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ jid: {
125
+ type: "string",
126
+ description: "Job ID of the failed job to delete"
127
+ }
128
+ },
129
+ required: ["jid"]
130
+ }
131
+ }
132
+ @tool_classes["delete_failed_job"] = Class.new(Tool) do
133
+ def perform(jid:)
134
+ retry_set = Sidekiq::RetrySet.new
135
+ job = retry_set.find_job(jid)
136
+
137
+ if job
138
+ job.delete
139
+ "Job #{jid} has been deleted successfully"
140
+ else
141
+ "Job #{jid} not found in retry set"
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Mcp
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mcp/version"
4
+ require_relative "mcp/tool"
5
+ require_relative "mcp/server"
6
+ require_relative "mcp/tools"
7
+ require_relative "mcp/middleware"
8
+ require_relative "mcp/sse_middleware"
9
+ require_relative "mcp/routes"
10
+
11
+ # Load all tool classes
12
+ Dir[File.join(__dir__, "mcp/tools/*.rb")].each { |file| require file }
13
+
14
+ if defined?(Rails)
15
+ require_relative "mcp/railtie"
16
+ end
17
+
18
+ module Sidekiq
19
+ module Mcp
20
+ class Error < StandardError; end
21
+
22
+ class << self
23
+ attr_accessor :configuration
24
+ end
25
+
26
+ def self.configure
27
+ self.configuration ||= Configuration.new
28
+ yield(configuration)
29
+ end
30
+
31
+ class Configuration
32
+ attr_accessor :enabled, :path, :auth_token, :sse_enabled
33
+
34
+ def initialize
35
+ @enabled = true
36
+ @path = "/sidekiq-mcp"
37
+ @auth_token = nil
38
+ @sse_enabled = true
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ module Sidekiq
2
+ module Mcp
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-mcp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Nesbitt
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sidekiq
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rack
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: base64
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: dry-schema
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.13'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.13'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rack-test
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: A Sidekiq plugin that provides an MCP (Model Context Protocol) server
83
+ for LLMs to interact with Sidekiq queues, stats, and failed jobs
84
+ email:
85
+ - andrewnez@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - CHANGELOG.md
91
+ - CODE_OF_CONDUCT.md
92
+ - README.md
93
+ - Rakefile
94
+ - lib/sidekiq/mcp.rb
95
+ - lib/sidekiq/mcp/middleware.rb
96
+ - lib/sidekiq/mcp/railtie.rb
97
+ - lib/sidekiq/mcp/routes.rb
98
+ - lib/sidekiq/mcp/server.rb
99
+ - lib/sidekiq/mcp/sse_middleware.rb
100
+ - lib/sidekiq/mcp/tool.rb
101
+ - lib/sidekiq/mcp/tools.rb
102
+ - lib/sidekiq/mcp/tools/clear_queue_tool.rb
103
+ - lib/sidekiq/mcp/tools/dead_jobs_tool.rb
104
+ - lib/sidekiq/mcp/tools/failed_jobs_tool.rb
105
+ - lib/sidekiq/mcp/tools/job_class_stats_tool.rb
106
+ - lib/sidekiq/mcp/tools/job_details_tool.rb
107
+ - lib/sidekiq/mcp/tools/kill_job_tool.rb
108
+ - lib/sidekiq/mcp/tools/list_retry_jobs_tool.rb
109
+ - lib/sidekiq/mcp/tools/list_scheduled_jobs_tool.rb
110
+ - lib/sidekiq/mcp/tools/process_set_tool.rb
111
+ - lib/sidekiq/mcp/tools/queue_details_tool.rb
112
+ - lib/sidekiq/mcp/tools/queue_health_tool.rb
113
+ - lib/sidekiq/mcp/tools/remove_job_tool.rb
114
+ - lib/sidekiq/mcp/tools/reschedule_job_tool.rb
115
+ - lib/sidekiq/mcp/tools/retry_job_tool.rb
116
+ - lib/sidekiq/mcp/tools/sidekiq_stats_tool.rb
117
+ - lib/sidekiq/mcp/tools/stream_stats_tool.rb
118
+ - lib/sidekiq/mcp/tools/workers_count_tool.rb
119
+ - lib/sidekiq/mcp/version.rb
120
+ - sig/sidekiq/mcp.rbs
121
+ homepage: https://github.com/andrew/sidekiq-mcp
122
+ licenses: []
123
+ metadata:
124
+ homepage_uri: https://github.com/andrew/sidekiq-mcp
125
+ source_code_uri: https://github.com/andrew/sidekiq-mcp
126
+ changelog_uri: https://github.com/andrew/sidekiq-mcp/blob/main/CHANGELOG.md
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 3.2.0
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubygems_version: 3.6.9
142
+ specification_version: 4
143
+ summary: Sidekiq MCP server plugin for LLM integration
144
+ test_files: []