model-context-protocol-rb 0.3.4 → 0.5.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 +4 -4
- data/CHANGELOG.md +26 -1
- data/README.md +886 -196
- data/lib/model_context_protocol/server/cancellable.rb +54 -0
- data/lib/model_context_protocol/server/configuration.rb +80 -8
- data/lib/model_context_protocol/server/content.rb +321 -0
- data/lib/model_context_protocol/server/content_helpers.rb +84 -0
- data/lib/model_context_protocol/server/pagination.rb +71 -0
- data/lib/model_context_protocol/server/progressable.rb +72 -0
- data/lib/model_context_protocol/server/prompt.rb +108 -14
- data/lib/model_context_protocol/server/redis_client_proxy.rb +134 -0
- data/lib/model_context_protocol/server/redis_config.rb +108 -0
- data/lib/model_context_protocol/server/redis_pool_manager.rb +110 -0
- data/lib/model_context_protocol/server/registry.rb +94 -18
- data/lib/model_context_protocol/server/resource.rb +98 -25
- data/lib/model_context_protocol/server/resource_template.rb +26 -13
- data/lib/model_context_protocol/server/router.rb +36 -3
- data/lib/model_context_protocol/server/stdio_transport/request_store.rb +102 -0
- data/lib/model_context_protocol/server/stdio_transport.rb +31 -6
- data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +35 -0
- data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +101 -0
- data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +80 -0
- data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +224 -0
- data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +120 -0
- data/lib/model_context_protocol/server/{session_store.rb → streamable_http_transport/session_store.rb} +30 -16
- data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +119 -0
- data/lib/model_context_protocol/server/streamable_http_transport.rb +352 -112
- data/lib/model_context_protocol/server/tool.rb +79 -53
- data/lib/model_context_protocol/server.rb +124 -21
- data/lib/model_context_protocol/version.rb +1 -1
- data/tasks/mcp.rake +28 -2
- data/tasks/templates/dev-http.erb +288 -0
- data/tasks/templates/dev.erb +7 -1
- metadata +61 -3
@@ -0,0 +1,72 @@
|
|
1
|
+
require "concurrent-ruby"
|
2
|
+
|
3
|
+
module ModelContextProtocol
|
4
|
+
module Server::Progressable
|
5
|
+
# Execute a block with automatic time-based progress reporting.
|
6
|
+
# Uses Concurrent::TimerTask to send progress notifications at regular intervals.
|
7
|
+
#
|
8
|
+
# @param max_duration [Numeric] Expected duration in seconds
|
9
|
+
# @param message [String, nil] Optional custom progress message
|
10
|
+
# @yield block to execute with progress tracking
|
11
|
+
# @return [Object] the result of the block
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# progressable(max_duration: 30) do # 30 seconds
|
15
|
+
# perform_long_operation
|
16
|
+
# end
|
17
|
+
def progressable(max_duration:, message: nil, &block)
|
18
|
+
context = Thread.current[:mcp_context]
|
19
|
+
|
20
|
+
return yield unless context && context[:progress_token] && context[:transport]
|
21
|
+
|
22
|
+
progress_token = context[:progress_token]
|
23
|
+
transport = context[:transport]
|
24
|
+
start_time = Time.now
|
25
|
+
update_interval = [1.0, max_duration * 0.05].max
|
26
|
+
|
27
|
+
timer_task = Concurrent::TimerTask.new(execution_interval: update_interval) do
|
28
|
+
elapsed_seconds = Time.now - start_time
|
29
|
+
progress_pct = [(elapsed_seconds / max_duration) * 100, 99].min
|
30
|
+
|
31
|
+
progress_message = if message
|
32
|
+
"#{message} (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
33
|
+
else
|
34
|
+
"Processing... (#{elapsed_seconds.round(1)}s / ~#{max_duration}s)"
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
transport.send_notification("notifications/progress", {
|
39
|
+
progressToken: progress_token,
|
40
|
+
progress: progress_pct.round(1),
|
41
|
+
total: 100,
|
42
|
+
message: progress_message
|
43
|
+
})
|
44
|
+
rescue
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
timer_task.shutdown if elapsed_seconds >= max_duration
|
49
|
+
end
|
50
|
+
|
51
|
+
begin
|
52
|
+
timer_task.execute
|
53
|
+
result = yield
|
54
|
+
|
55
|
+
begin
|
56
|
+
transport.send_notification("notifications/progress", {
|
57
|
+
progressToken: progress_token,
|
58
|
+
progress: 100,
|
59
|
+
total: 100,
|
60
|
+
message: "Completed"
|
61
|
+
})
|
62
|
+
rescue
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
result
|
67
|
+
ensure
|
68
|
+
timer_task&.shutdown if timer_task&.running?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
module ModelContextProtocol
|
2
2
|
class Server::Prompt
|
3
|
+
include ModelContextProtocol::Server::Cancellable
|
4
|
+
include ModelContextProtocol::Server::ContentHelpers
|
5
|
+
include ModelContextProtocol::Server::Progressable
|
6
|
+
|
3
7
|
attr_reader :arguments, :context, :logger
|
4
8
|
|
5
9
|
def initialize(arguments, logger, context = {})
|
@@ -13,15 +17,23 @@ module ModelContextProtocol
|
|
13
17
|
raise NotImplementedError, "Subclasses must implement the call method"
|
14
18
|
end
|
15
19
|
|
16
|
-
Response = Data.define(:messages, :description) do
|
20
|
+
Response = Data.define(:messages, :description, :title) do
|
17
21
|
def serialized
|
18
|
-
{description:, messages:}
|
22
|
+
result = {description:, messages:}
|
23
|
+
result[:title] = title if title
|
24
|
+
result
|
19
25
|
end
|
20
26
|
end
|
21
27
|
private_constant :Response
|
22
28
|
|
29
|
+
def message_history(&block)
|
30
|
+
builder = MessageHistoryBuilder.new(self)
|
31
|
+
builder.instance_eval(&block)
|
32
|
+
builder.messages
|
33
|
+
end
|
34
|
+
|
23
35
|
private def respond_with(messages:)
|
24
|
-
Response[messages:, description: self.class.description]
|
36
|
+
Response[messages:, description: self.class.description, title: self.class.title]
|
25
37
|
end
|
26
38
|
|
27
39
|
private def validate!(arguments = {})
|
@@ -43,16 +55,18 @@ module ModelContextProtocol
|
|
43
55
|
end
|
44
56
|
|
45
57
|
class << self
|
46
|
-
attr_reader :name, :description, :defined_arguments
|
58
|
+
attr_reader :name, :description, :title, :defined_arguments
|
47
59
|
|
48
|
-
def
|
60
|
+
def define(&block)
|
49
61
|
@defined_arguments ||= []
|
50
62
|
|
51
|
-
|
52
|
-
|
63
|
+
definition_dsl = DefinitionDSL.new
|
64
|
+
definition_dsl.instance_eval(&block)
|
53
65
|
|
54
|
-
@name =
|
55
|
-
@description =
|
66
|
+
@name = definition_dsl.name
|
67
|
+
@description = definition_dsl.description
|
68
|
+
@title = definition_dsl.title
|
69
|
+
@defined_arguments.concat(definition_dsl.arguments)
|
56
70
|
end
|
57
71
|
|
58
72
|
def with_argument(&block)
|
@@ -72,6 +86,7 @@ module ModelContextProtocol
|
|
72
86
|
def inherited(subclass)
|
73
87
|
subclass.instance_variable_set(:@name, @name)
|
74
88
|
subclass.instance_variable_set(:@description, @description)
|
89
|
+
subclass.instance_variable_set(:@title, @title)
|
75
90
|
subclass.instance_variable_set(:@defined_arguments, @defined_arguments&.dup)
|
76
91
|
end
|
77
92
|
|
@@ -81,8 +96,10 @@ module ModelContextProtocol
|
|
81
96
|
raise ModelContextProtocol::Server::ParameterValidationError, error.message
|
82
97
|
end
|
83
98
|
|
84
|
-
def
|
85
|
-
{name: @name, description: @description, arguments: @defined_arguments}
|
99
|
+
def definition
|
100
|
+
result = {name: @name, description: @description, arguments: @defined_arguments}
|
101
|
+
result[:title] = @title if @title
|
102
|
+
result
|
86
103
|
end
|
87
104
|
|
88
105
|
def complete_for(arg_name, value)
|
@@ -92,7 +109,52 @@ module ModelContextProtocol
|
|
92
109
|
end
|
93
110
|
end
|
94
111
|
|
95
|
-
class
|
112
|
+
class MessageHistoryBuilder
|
113
|
+
include Server::ContentHelpers
|
114
|
+
|
115
|
+
attr_reader :messages
|
116
|
+
|
117
|
+
def initialize(prompt_instance)
|
118
|
+
@messages = []
|
119
|
+
@prompt_instance = prompt_instance
|
120
|
+
end
|
121
|
+
|
122
|
+
def arguments
|
123
|
+
@prompt_instance.arguments
|
124
|
+
end
|
125
|
+
|
126
|
+
def context
|
127
|
+
@prompt_instance.context
|
128
|
+
end
|
129
|
+
|
130
|
+
def logger
|
131
|
+
@prompt_instance.logger
|
132
|
+
end
|
133
|
+
|
134
|
+
def user_message(&block)
|
135
|
+
content = instance_eval(&block).serialized
|
136
|
+
@messages << {
|
137
|
+
role: "user",
|
138
|
+
content: content
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def assistant_message(&block)
|
143
|
+
content = instance_eval(&block).serialized
|
144
|
+
@messages << {
|
145
|
+
role: "assistant",
|
146
|
+
content: content
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class DefinitionDSL
|
152
|
+
attr_reader :arguments
|
153
|
+
|
154
|
+
def initialize
|
155
|
+
@arguments = []
|
156
|
+
end
|
157
|
+
|
96
158
|
def name(value = nil)
|
97
159
|
@name = value if value
|
98
160
|
@name
|
@@ -102,6 +164,23 @@ module ModelContextProtocol
|
|
102
164
|
@description = value if value
|
103
165
|
@description
|
104
166
|
end
|
167
|
+
|
168
|
+
def title(value = nil)
|
169
|
+
@title = value if value
|
170
|
+
@title
|
171
|
+
end
|
172
|
+
|
173
|
+
def argument(&block)
|
174
|
+
argument_dsl = ArgumentDSL.new
|
175
|
+
argument_dsl.instance_eval(&block)
|
176
|
+
|
177
|
+
@arguments << {
|
178
|
+
name: argument_dsl.name,
|
179
|
+
description: argument_dsl.description,
|
180
|
+
required: argument_dsl.required,
|
181
|
+
completion: argument_dsl.completion
|
182
|
+
}
|
183
|
+
end
|
105
184
|
end
|
106
185
|
|
107
186
|
class ArgumentDSL
|
@@ -120,10 +199,25 @@ module ModelContextProtocol
|
|
120
199
|
@required
|
121
200
|
end
|
122
201
|
|
123
|
-
def completion(
|
124
|
-
|
202
|
+
def completion(klass_or_values = nil)
|
203
|
+
unless klass_or_values.nil?
|
204
|
+
@completion = if klass_or_values.is_a?(Array)
|
205
|
+
create_array_completion(klass_or_values)
|
206
|
+
else
|
207
|
+
klass_or_values
|
208
|
+
end
|
209
|
+
end
|
125
210
|
@completion
|
126
211
|
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def create_array_completion(values)
|
216
|
+
ModelContextProtocol::Server::Completion.define do
|
217
|
+
filtered_values = values.grep(/#{argument_value}/)
|
218
|
+
respond_with values: filtered_values
|
219
|
+
end
|
220
|
+
end
|
127
221
|
end
|
128
222
|
end
|
129
223
|
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ModelContextProtocol
|
4
|
+
class Server
|
5
|
+
class RedisClientProxy
|
6
|
+
def initialize(pool)
|
7
|
+
@pool = pool
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(key)
|
11
|
+
with_connection { |redis| redis.get(key) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def set(key, value, **options)
|
15
|
+
with_connection { |redis| redis.set(key, value, **options) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def del(*keys)
|
19
|
+
with_connection { |redis| redis.del(*keys) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def exists(*keys)
|
23
|
+
with_connection { |redis| redis.exists(*keys) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def expire(key, seconds)
|
27
|
+
with_connection { |redis| redis.expire(key, seconds) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def ttl(key)
|
31
|
+
with_connection { |redis| redis.ttl(key) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def hget(key, field)
|
35
|
+
with_connection { |redis| redis.hget(key, field) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def hset(key, *args)
|
39
|
+
with_connection { |redis| redis.hset(key, *args) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def hgetall(key)
|
43
|
+
with_connection { |redis| redis.hgetall(key) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def lpush(key, *values)
|
47
|
+
with_connection { |redis| redis.lpush(key, *values) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def rpop(key)
|
51
|
+
with_connection { |redis| redis.rpop(key) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def lrange(key, start, stop)
|
55
|
+
with_connection { |redis| redis.lrange(key, start, stop) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def llen(key)
|
59
|
+
with_connection { |redis| redis.llen(key) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def ltrim(key, start, stop)
|
63
|
+
with_connection { |redis| redis.ltrim(key, start, stop) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def incr(key)
|
67
|
+
with_connection { |redis| redis.incr(key) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def decr(key)
|
71
|
+
with_connection { |redis| redis.decr(key) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def keys(pattern)
|
75
|
+
with_connection { |redis| redis.keys(pattern) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def multi(&block)
|
79
|
+
with_connection do |redis|
|
80
|
+
redis.multi do |multi|
|
81
|
+
multi_wrapper = RedisMultiWrapper.new(multi)
|
82
|
+
block.call(multi_wrapper)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def pipelined(&block)
|
88
|
+
with_connection do |redis|
|
89
|
+
redis.pipelined do |pipeline|
|
90
|
+
pipeline_wrapper = RedisMultiWrapper.new(pipeline)
|
91
|
+
block.call(pipeline_wrapper)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def mget(*keys)
|
97
|
+
with_connection { |redis| redis.mget(*keys) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def eval(script, keys: [], argv: [])
|
101
|
+
with_connection { |redis| redis.eval(script, keys: keys, argv: argv) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def ping
|
105
|
+
with_connection { |redis| redis.ping }
|
106
|
+
end
|
107
|
+
|
108
|
+
def flushdb
|
109
|
+
with_connection { |redis| redis.flushdb }
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def with_connection(&block)
|
115
|
+
@pool.with(&block)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Wrapper for Redis multi/pipeline operations
|
119
|
+
class RedisMultiWrapper
|
120
|
+
def initialize(multi)
|
121
|
+
@multi = multi
|
122
|
+
end
|
123
|
+
|
124
|
+
def method_missing(method, *args, **kwargs, &block)
|
125
|
+
@multi.send(method, *args, **kwargs, &block)
|
126
|
+
end
|
127
|
+
|
128
|
+
def respond_to_missing?(method, include_private = false)
|
129
|
+
@multi.respond_to?(method, include_private)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "singleton"
|
2
|
+
|
3
|
+
module ModelContextProtocol
|
4
|
+
class Server::RedisConfig
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
class NotConfiguredError < StandardError
|
8
|
+
def initialize
|
9
|
+
super("Redis not configured. Call ModelContextProtocol::Server.configure_redis first")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :manager
|
14
|
+
|
15
|
+
def self.configure(&block)
|
16
|
+
instance.configure(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configured?
|
20
|
+
instance.configured?
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.pool
|
24
|
+
instance.pool
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.shutdown!
|
28
|
+
instance.shutdown!
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.reset!
|
32
|
+
instance.reset!
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.stats
|
36
|
+
instance.stats
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.pool_manager
|
40
|
+
instance.manager
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
reset!
|
45
|
+
end
|
46
|
+
|
47
|
+
def configure(&block)
|
48
|
+
shutdown! if configured?
|
49
|
+
|
50
|
+
config = Configuration.new
|
51
|
+
yield(config) if block_given?
|
52
|
+
|
53
|
+
@manager = Server::RedisPoolManager.new(
|
54
|
+
redis_url: config.redis_url,
|
55
|
+
pool_size: config.pool_size,
|
56
|
+
pool_timeout: config.pool_timeout
|
57
|
+
)
|
58
|
+
|
59
|
+
if config.enable_reaper
|
60
|
+
@manager.configure_reaper(
|
61
|
+
enabled: true,
|
62
|
+
interval: config.reaper_interval,
|
63
|
+
idle_timeout: config.idle_timeout
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
@manager.start
|
68
|
+
end
|
69
|
+
|
70
|
+
def configured?
|
71
|
+
!@manager.nil? && !@manager.pool.nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
def pool
|
75
|
+
raise NotConfiguredError unless configured?
|
76
|
+
@manager.pool
|
77
|
+
end
|
78
|
+
|
79
|
+
def shutdown!
|
80
|
+
@manager&.shutdown
|
81
|
+
@manager = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def reset!
|
85
|
+
shutdown!
|
86
|
+
@manager = nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def stats
|
90
|
+
return {} unless configured?
|
91
|
+
@manager.stats
|
92
|
+
end
|
93
|
+
|
94
|
+
class Configuration
|
95
|
+
attr_accessor :redis_url, :pool_size, :pool_timeout,
|
96
|
+
:enable_reaper, :reaper_interval, :idle_timeout
|
97
|
+
|
98
|
+
def initialize
|
99
|
+
@redis_url = nil
|
100
|
+
@pool_size = 20
|
101
|
+
@pool_timeout = 5
|
102
|
+
@enable_reaper = true
|
103
|
+
@reaper_interval = 60
|
104
|
+
@idle_timeout = 300
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module ModelContextProtocol
|
2
|
+
class Server::RedisPoolManager
|
3
|
+
attr_reader :pool, :reaper_thread
|
4
|
+
|
5
|
+
def initialize(redis_url:, pool_size: 20, pool_timeout: 5)
|
6
|
+
@redis_url = redis_url
|
7
|
+
@pool_size = pool_size
|
8
|
+
@pool_timeout = pool_timeout
|
9
|
+
@pool = nil
|
10
|
+
@reaper_thread = nil
|
11
|
+
@reaper_config = {
|
12
|
+
enabled: false,
|
13
|
+
interval: 60,
|
14
|
+
idle_timeout: 300
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure_reaper(enabled:, interval: 60, idle_timeout: 300)
|
19
|
+
@reaper_config = {
|
20
|
+
enabled: enabled,
|
21
|
+
interval: interval,
|
22
|
+
idle_timeout: idle_timeout
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
validate!
|
28
|
+
create_pool
|
29
|
+
start_reaper if @reaper_config[:enabled]
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def shutdown
|
34
|
+
stop_reaper
|
35
|
+
close_pool
|
36
|
+
end
|
37
|
+
|
38
|
+
def healthy?
|
39
|
+
return false unless @pool
|
40
|
+
|
41
|
+
@pool.with do |conn|
|
42
|
+
conn.ping == "PONG"
|
43
|
+
end
|
44
|
+
rescue
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def reap_now
|
49
|
+
return unless @pool
|
50
|
+
|
51
|
+
@pool.reap(@reaper_config[:idle_timeout]) do |conn|
|
52
|
+
conn.close
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def stats
|
57
|
+
return {} unless @pool
|
58
|
+
|
59
|
+
{
|
60
|
+
size: @pool.size,
|
61
|
+
available: @pool.available,
|
62
|
+
idle: @pool.instance_variable_get(:@idle_since)&.size || 0
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def validate!
|
69
|
+
raise ArgumentError, "redis_url is required" if @redis_url.nil? || @redis_url.empty?
|
70
|
+
raise ArgumentError, "pool_size must be positive" if @pool_size <= 0
|
71
|
+
raise ArgumentError, "pool_timeout must be positive" if @pool_timeout <= 0
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_pool
|
75
|
+
@pool = ConnectionPool.new(size: @pool_size, timeout: @pool_timeout) do
|
76
|
+
Redis.new(url: @redis_url)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def close_pool
|
81
|
+
@pool&.shutdown { |conn| conn.close }
|
82
|
+
@pool = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def start_reaper
|
86
|
+
return if @reaper_thread&.alive?
|
87
|
+
|
88
|
+
@reaper_thread = Thread.new do
|
89
|
+
loop do
|
90
|
+
sleep @reaper_config[:interval]
|
91
|
+
begin
|
92
|
+
reap_now
|
93
|
+
rescue => e
|
94
|
+
warn "Redis reaper error: #{e.message}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
@reaper_thread.name = "MCP-Redis-Reaper"
|
100
|
+
end
|
101
|
+
|
102
|
+
def stop_reaper
|
103
|
+
return unless @reaper_thread&.alive?
|
104
|
+
|
105
|
+
@reaper_thread.kill
|
106
|
+
@reaper_thread.join(5)
|
107
|
+
@reaper_thread = nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|