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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +207 -0
- data/Rakefile +8 -0
- data/lib/sidekiq/mcp/middleware.rb +105 -0
- data/lib/sidekiq/mcp/railtie.rb +21 -0
- data/lib/sidekiq/mcp/routes.rb +17 -0
- data/lib/sidekiq/mcp/server.rb +101 -0
- data/lib/sidekiq/mcp/sse_middleware.rb +181 -0
- data/lib/sidekiq/mcp/tool.rb +56 -0
- data/lib/sidekiq/mcp/tools/clear_queue_tool.rb +30 -0
- data/lib/sidekiq/mcp/tools/dead_jobs_tool.rb +39 -0
- data/lib/sidekiq/mcp/tools/failed_jobs_tool.rb +40 -0
- data/lib/sidekiq/mcp/tools/job_class_stats_tool.rb +56 -0
- data/lib/sidekiq/mcp/tools/job_details_tool.rb +74 -0
- data/lib/sidekiq/mcp/tools/kill_job_tool.rb +40 -0
- data/lib/sidekiq/mcp/tools/list_retry_jobs_tool.rb +40 -0
- data/lib/sidekiq/mcp/tools/list_scheduled_jobs_tool.rb +36 -0
- data/lib/sidekiq/mcp/tools/process_set_tool.rb +43 -0
- data/lib/sidekiq/mcp/tools/queue_details_tool.rb +40 -0
- data/lib/sidekiq/mcp/tools/queue_health_tool.rb +79 -0
- data/lib/sidekiq/mcp/tools/remove_job_tool.rb +51 -0
- data/lib/sidekiq/mcp/tools/reschedule_job_tool.rb +38 -0
- data/lib/sidekiq/mcp/tools/retry_job_tool.rb +30 -0
- data/lib/sidekiq/mcp/tools/sidekiq_stats_tool.rb +32 -0
- data/lib/sidekiq/mcp/tools/stream_stats_tool.rb +45 -0
- data/lib/sidekiq/mcp/tools/workers_count_tool.rb +52 -0
- data/lib/sidekiq/mcp/tools.rb +148 -0
- data/lib/sidekiq/mcp/version.rb +7 -0
- data/lib/sidekiq/mcp.rb +42 -0
- data/sig/sidekiq/mcp.rbs +6 -0
- metadata +144 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "base64"
|
5
|
+
require "json"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
module Sidekiq
|
9
|
+
module Mcp
|
10
|
+
class SseMiddleware
|
11
|
+
SSE_HEADERS = {
|
12
|
+
'Content-Type' => 'text/event-stream',
|
13
|
+
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
14
|
+
'Connection' => 'keep-alive',
|
15
|
+
'X-Accel-Buffering' => 'no', # For Nginx
|
16
|
+
'Access-Control-Allow-Origin' => '*',
|
17
|
+
'Access-Control-Allow-Methods' => 'GET, OPTIONS',
|
18
|
+
'Access-Control-Allow-Headers' => 'Content-Type, Authorization',
|
19
|
+
'Access-Control-Max-Age' => '86400',
|
20
|
+
'Keep-Alive' => 'timeout=600',
|
21
|
+
'Pragma' => 'no-cache',
|
22
|
+
'Expires' => '0'
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
def initialize(app)
|
26
|
+
@app = app
|
27
|
+
@server = Server.new
|
28
|
+
@clients = {}
|
29
|
+
@clients_mutex = Mutex.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
request = Rack::Request.new(env)
|
34
|
+
|
35
|
+
return @app.call(env) unless sse_request?(request)
|
36
|
+
return unauthorized_response unless authorized?(request)
|
37
|
+
|
38
|
+
if request.get?
|
39
|
+
handle_sse_connection(request, env)
|
40
|
+
elsif request.post?
|
41
|
+
handle_mcp_message(request)
|
42
|
+
else
|
43
|
+
method_not_allowed_response
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def sse_request?(request)
|
50
|
+
config = Sidekiq::Mcp.configuration || Sidekiq::Mcp::Configuration.new
|
51
|
+
request.path == "#{config.path}/sse"
|
52
|
+
end
|
53
|
+
|
54
|
+
def authorized?(request)
|
55
|
+
config = Sidekiq::Mcp.configuration || Sidekiq::Mcp::Configuration.new
|
56
|
+
|
57
|
+
return true unless config.auth_token
|
58
|
+
|
59
|
+
auth_header = request.env["HTTP_AUTHORIZATION"]
|
60
|
+
return false unless auth_header
|
61
|
+
|
62
|
+
if auth_header.start_with?("Bearer ")
|
63
|
+
token = auth_header.split(" ", 2).last
|
64
|
+
token == config.auth_token
|
65
|
+
elsif auth_header.start_with?("Basic ")
|
66
|
+
encoded = auth_header.split(" ", 2).last
|
67
|
+
decoded = Base64.decode64(encoded)
|
68
|
+
_username, password = decoded.split(":", 2)
|
69
|
+
password == config.auth_token
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_sse_connection(request, env)
|
76
|
+
client_id = SecureRandom.hex(8)
|
77
|
+
|
78
|
+
[200, SSE_HEADERS, SseStreamer.new(client_id, self)]
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle_mcp_message(request)
|
82
|
+
body = request.body.read
|
83
|
+
response = @server.handle_request(body)
|
84
|
+
|
85
|
+
# Broadcast to all SSE clients
|
86
|
+
broadcast_to_clients(response) if response
|
87
|
+
|
88
|
+
if response.nil?
|
89
|
+
[204, {}, []]
|
90
|
+
else
|
91
|
+
[200, {"Content-Type" => "application/json"}, [response.to_json]]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def broadcast_to_clients(message)
|
96
|
+
@clients_mutex.synchronize do
|
97
|
+
@clients.each_value do |client_data|
|
98
|
+
begin
|
99
|
+
client_data[:stream].write("data: #{message.to_json}\n\n")
|
100
|
+
client_data[:stream].flush
|
101
|
+
rescue
|
102
|
+
# Client disconnected, will be cleaned up later
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_client(client_id, stream)
|
109
|
+
@clients_mutex.synchronize do
|
110
|
+
@clients[client_id] = { stream: stream, connected_at: Time.now }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def remove_client(client_id)
|
115
|
+
@clients_mutex.synchronize do
|
116
|
+
@clients.delete(client_id)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def unauthorized_response
|
121
|
+
[401, { "Content-Type" => "application/json" }, [{ error: "Unauthorized" }.to_json]]
|
122
|
+
end
|
123
|
+
|
124
|
+
def method_not_allowed_response
|
125
|
+
[405, { "Content-Type" => "application/json" }, [{ error: "Method not allowed" }.to_json]]
|
126
|
+
end
|
127
|
+
|
128
|
+
class SseStreamer
|
129
|
+
def initialize(client_id, middleware)
|
130
|
+
@client_id = client_id
|
131
|
+
@middleware = middleware
|
132
|
+
end
|
133
|
+
|
134
|
+
def each
|
135
|
+
@middleware.add_client(@client_id, self)
|
136
|
+
|
137
|
+
# Send initial connection message
|
138
|
+
yield "data: #{initial_message.to_json}\n\n"
|
139
|
+
|
140
|
+
# Keep connection alive with heartbeats
|
141
|
+
begin
|
142
|
+
loop do
|
143
|
+
sleep 30
|
144
|
+
yield ": heartbeat\n\n"
|
145
|
+
end
|
146
|
+
rescue
|
147
|
+
# Connection closed
|
148
|
+
ensure
|
149
|
+
@middleware.remove_client(@client_id)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def write(data)
|
154
|
+
@data = data
|
155
|
+
end
|
156
|
+
|
157
|
+
def flush
|
158
|
+
# This method is called by broadcast_to_clients
|
159
|
+
# In a real streaming setup, we'd need to queue messages
|
160
|
+
# For now, this is a simplified implementation
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def initial_message
|
166
|
+
{
|
167
|
+
jsonrpc: "2.0",
|
168
|
+
method: "connection/established",
|
169
|
+
params: {
|
170
|
+
client_id: @client_id,
|
171
|
+
server_info: {
|
172
|
+
name: "sidekiq-mcp",
|
173
|
+
version: Sidekiq::Mcp::VERSION
|
174
|
+
}
|
175
|
+
}
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-schema"
|
4
|
+
|
5
|
+
module Sidekiq
|
6
|
+
module Mcp
|
7
|
+
class Tool
|
8
|
+
class << self
|
9
|
+
attr_accessor :tool_description, :argument_schema
|
10
|
+
|
11
|
+
def description(text)
|
12
|
+
@tool_description = text
|
13
|
+
end
|
14
|
+
|
15
|
+
def arguments(&block)
|
16
|
+
@argument_schema = Dry::Schema.Params(&block) if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def schema_to_json_schema
|
20
|
+
return { type: "object", properties: {}, required: [] } unless @argument_schema
|
21
|
+
|
22
|
+
# For now, return a simple schema - can be enhanced later
|
23
|
+
{
|
24
|
+
type: "object",
|
25
|
+
properties: {},
|
26
|
+
required: []
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.to_tool_definition
|
32
|
+
tool_name = name.split("::").last.gsub(/Tool$/, "").gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
|
33
|
+
{
|
34
|
+
name: tool_name,
|
35
|
+
description: @tool_description || "#{name} tool",
|
36
|
+
inputSchema: schema_to_json_schema
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(**args)
|
41
|
+
# Validate arguments if schema is defined
|
42
|
+
if self.class.argument_schema
|
43
|
+
result = self.class.argument_schema.call(args)
|
44
|
+
raise ArgumentError, "Invalid arguments: #{result.errors.to_h}" unless result.success?
|
45
|
+
args = result.to_h
|
46
|
+
end
|
47
|
+
|
48
|
+
perform(**args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def perform(**args)
|
52
|
+
raise NotImplementedError, "Subclasses must implement #perform"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
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 ClearQueueTool < Tool
|
10
|
+
description "Clear all jobs from a specific queue (destructive operation)"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
required(:queue_name).filled(:string)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(queue_name:)
|
17
|
+
queue = Sidekiq::Queue.new(queue_name)
|
18
|
+
initial_size = queue.size
|
19
|
+
|
20
|
+
if initial_size == 0
|
21
|
+
"Queue '#{queue_name}' is already empty"
|
22
|
+
else
|
23
|
+
queue.clear
|
24
|
+
"Cleared #{initial_size} jobs from queue '#{queue_name}'"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
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 DeadJobsTool < Tool
|
10
|
+
description "Show jobs in the dead set (jobs that have exhausted all retries)"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
optional(:limit).filled(:integer).value(gteq?: 1, lteq?: 100)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(limit: 10)
|
17
|
+
jobs = Sidekiq::DeadSet.new.first(limit).map do |job|
|
18
|
+
{
|
19
|
+
jid: job.jid,
|
20
|
+
class: job.klass,
|
21
|
+
args: job.args,
|
22
|
+
queue: job.queue,
|
23
|
+
error_message: job["error_message"],
|
24
|
+
error_class: job["error_class"],
|
25
|
+
failed_at: job["failed_at"],
|
26
|
+
retry_count: job["retry_count"],
|
27
|
+
died_at: job["died_at"]
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
{
|
32
|
+
total_size: Sidekiq::DeadSet.new.size,
|
33
|
+
jobs: jobs
|
34
|
+
}.to_json
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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 FailedJobsTool < Tool
|
10
|
+
description "List failed jobs with error details and retry information"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
optional(:limit).filled(:integer).value(gteq?: 1, lteq?: 100)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(limit: 10)
|
17
|
+
failed_set = Sidekiq::RetrySet.new
|
18
|
+
jobs = failed_set.first(limit).map do |job|
|
19
|
+
{
|
20
|
+
jid: job.jid,
|
21
|
+
class: job.klass,
|
22
|
+
args: job.args,
|
23
|
+
queue: job.queue,
|
24
|
+
error_message: job["error_message"],
|
25
|
+
error_class: job["error_class"],
|
26
|
+
failed_at: job["failed_at"],
|
27
|
+
retry_count: job["retry_count"],
|
28
|
+
retried_at: job["retried_at"]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
{
|
33
|
+
total_size: failed_set.size,
|
34
|
+
jobs: jobs
|
35
|
+
}.to_json
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,56 @@
|
|
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 JobClassStatsTool < Tool
|
10
|
+
description "Breakdown of job counts, retries, and error rates by class"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
# No arguments needed
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
stats = {}
|
18
|
+
|
19
|
+
# Get stats from retry set
|
20
|
+
Sidekiq::RetrySet.new.each do |job|
|
21
|
+
klass = job.klass
|
22
|
+
stats[klass] ||= { retry_count: 0, failed_count: 0, total_retries: 0 }
|
23
|
+
stats[klass][:retry_count] += 1
|
24
|
+
stats[klass][:total_retries] += (job["retry_count"] || 0)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get stats from dead set
|
28
|
+
Sidekiq::DeadSet.new.each do |job|
|
29
|
+
klass = job.klass
|
30
|
+
stats[klass] ||= { retry_count: 0, failed_count: 0, total_retries: 0 }
|
31
|
+
stats[klass][:failed_count] += 1
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get stats from queues
|
35
|
+
Sidekiq::Queue.all.each do |queue|
|
36
|
+
queue.each do |job|
|
37
|
+
klass = job.klass
|
38
|
+
stats[klass] ||= { retry_count: 0, failed_count: 0, total_retries: 0, enqueued_count: 0 }
|
39
|
+
stats[klass][:enqueued_count] = (stats[klass][:enqueued_count] || 0) + 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Calculate error rates
|
44
|
+
stats.each do |klass, data|
|
45
|
+
total_jobs = (data[:retry_count] || 0) + (data[:failed_count] || 0) + (data[:enqueued_count] || 0)
|
46
|
+
error_count = (data[:retry_count] || 0) + (data[:failed_count] || 0)
|
47
|
+
data[:error_rate] = total_jobs > 0 ? (error_count.to_f / total_jobs * 100).round(2) : 0
|
48
|
+
data[:total_jobs] = total_jobs
|
49
|
+
end
|
50
|
+
|
51
|
+
stats.to_json
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,74 @@
|
|
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 JobDetailsTool < Tool
|
10
|
+
description "Show args, error message, and history for a job by JID (failed, scheduled, or retry)"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
required(:jid).filled(:string)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(jid:)
|
17
|
+
job = find_job_by_jid(jid)
|
18
|
+
|
19
|
+
return "Job #{jid} not found" unless job
|
20
|
+
|
21
|
+
details = {
|
22
|
+
jid: job.jid,
|
23
|
+
class: job.klass,
|
24
|
+
args: job.args,
|
25
|
+
queue: job.queue,
|
26
|
+
created_at: job.created_at,
|
27
|
+
enqueued_at: job.enqueued_at
|
28
|
+
}
|
29
|
+
|
30
|
+
# Add retry/failure specific info
|
31
|
+
if job.respond_to?(:[])
|
32
|
+
details[:error_message] = job["error_message"] if job["error_message"]
|
33
|
+
details[:error_class] = job["error_class"] if job["error_class"]
|
34
|
+
details[:failed_at] = job["failed_at"] if job["failed_at"]
|
35
|
+
details[:retry_count] = job["retry_count"] if job["retry_count"]
|
36
|
+
details[:retried_at] = job["retried_at"] if job["retried_at"]
|
37
|
+
details[:backtrace] = job["error_backtrace"] if job["error_backtrace"]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add scheduled info
|
41
|
+
if job.respond_to?(:at)
|
42
|
+
details[:scheduled_at] = job.at
|
43
|
+
end
|
44
|
+
|
45
|
+
details.to_json
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def find_job_by_jid(jid)
|
51
|
+
# Check retry set
|
52
|
+
job = Sidekiq::RetrySet.new.find_job(jid)
|
53
|
+
return job if job
|
54
|
+
|
55
|
+
# Check scheduled set
|
56
|
+
job = Sidekiq::ScheduledSet.new.find_job(jid)
|
57
|
+
return job if job
|
58
|
+
|
59
|
+
# Check dead set
|
60
|
+
job = Sidekiq::DeadSet.new.find_job(jid)
|
61
|
+
return job if job
|
62
|
+
|
63
|
+
# Check all queues
|
64
|
+
Sidekiq::Queue.all.each do |queue|
|
65
|
+
job = queue.find_job(jid)
|
66
|
+
return job if job
|
67
|
+
end
|
68
|
+
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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 KillJobTool < Tool
|
10
|
+
description "Move a job from retry/scheduled set to the dead set"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
required(:jid).filled(:string)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(jid:)
|
17
|
+
# Try retry set first
|
18
|
+
retry_set = Sidekiq::RetrySet.new
|
19
|
+
job = retry_set.find_job(jid)
|
20
|
+
|
21
|
+
if job
|
22
|
+
job.kill
|
23
|
+
return "Job #{jid} moved from retry set to dead set"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Try scheduled set
|
27
|
+
scheduled_set = Sidekiq::ScheduledSet.new
|
28
|
+
job = scheduled_set.find_job(jid)
|
29
|
+
|
30
|
+
if job
|
31
|
+
job.kill
|
32
|
+
return "Job #{jid} moved from scheduled set to dead set"
|
33
|
+
end
|
34
|
+
|
35
|
+
"Job #{jid} not found in retry or scheduled sets"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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 ListRetryJobsTool < Tool
|
10
|
+
description "List jobs in the retry set (jobs that failed but will be retried)"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
optional(:limit).filled(:integer).value(gteq?: 1, lteq?: 100)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(limit: 10)
|
17
|
+
jobs = Sidekiq::RetrySet.new.first(limit).map do |job|
|
18
|
+
{
|
19
|
+
jid: job.jid,
|
20
|
+
class: job.klass,
|
21
|
+
args: job.args,
|
22
|
+
queue: job.queue,
|
23
|
+
error_message: job["error_message"],
|
24
|
+
error_class: job["error_class"],
|
25
|
+
failed_at: job["failed_at"],
|
26
|
+
retry_count: job["retry_count"],
|
27
|
+
retried_at: job["retried_at"],
|
28
|
+
next_retry_at: job.at
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
{
|
33
|
+
total_size: Sidekiq::RetrySet.new.size,
|
34
|
+
jobs: jobs
|
35
|
+
}.to_json
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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 ListScheduledJobsTool < Tool
|
10
|
+
description "List jobs in the scheduled set"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
optional(:limit).filled(:integer).value(gteq?: 1, lteq?: 100)
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(limit: 10)
|
17
|
+
jobs = Sidekiq::ScheduledSet.new.first(limit).map do |job|
|
18
|
+
{
|
19
|
+
jid: job.jid,
|
20
|
+
class: job.klass,
|
21
|
+
args: job.args,
|
22
|
+
queue: job.queue,
|
23
|
+
scheduled_at: job.at,
|
24
|
+
created_at: job.created_at
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
{
|
29
|
+
total_size: Sidekiq::ScheduledSet.new.size,
|
30
|
+
jobs: jobs
|
31
|
+
}.to_json
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,43 @@
|
|
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 ProcessSetTool < Tool
|
10
|
+
description "Get detailed information about all Sidekiq processes/workers"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
# No arguments needed
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
processes = Sidekiq::ProcessSet.new.map do |process|
|
18
|
+
{
|
19
|
+
identity: process["identity"],
|
20
|
+
hostname: process["hostname"],
|
21
|
+
pid: process["pid"],
|
22
|
+
tag: process["tag"],
|
23
|
+
concurrency: process["concurrency"],
|
24
|
+
queues: process["queues"],
|
25
|
+
busy: process["busy"],
|
26
|
+
beat: process["beat"],
|
27
|
+
quiet: process["quiet"],
|
28
|
+
started_at: process["started_at"],
|
29
|
+
labels: process["labels"],
|
30
|
+
version: process["version"],
|
31
|
+
rss_kb: process["rss"]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
{
|
36
|
+
total_processes: processes.size,
|
37
|
+
processes: processes
|
38
|
+
}.to_json
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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 QueueDetailsTool < Tool
|
10
|
+
description "Get detailed information about a specific queue including jobs"
|
11
|
+
|
12
|
+
arguments do
|
13
|
+
required(:queue_name).filled(:string)
|
14
|
+
optional(:limit).filled(:integer).value(gteq?: 1, lteq?: 100)
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform(queue_name:, limit: 10)
|
18
|
+
queue = Sidekiq::Queue.new(queue_name)
|
19
|
+
jobs = queue.first(limit).map do |job|
|
20
|
+
{
|
21
|
+
jid: job.jid,
|
22
|
+
class: job.klass,
|
23
|
+
args: job.args,
|
24
|
+
created_at: job.created_at,
|
25
|
+
enqueued_at: job.enqueued_at,
|
26
|
+
queue: job.queue
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
{
|
31
|
+
queue_name: queue_name,
|
32
|
+
size: queue.size,
|
33
|
+
latency: queue.latency,
|
34
|
+
jobs: jobs
|
35
|
+
}.to_json
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|