model-context-protocol-rb 0.5.1 → 0.7.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 +37 -1
- data/README.md +181 -950
- data/lib/model_context_protocol/rspec/helpers.rb +54 -0
- data/lib/model_context_protocol/rspec/matchers/be_mcp_error_response.rb +123 -0
- data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_class.rb +103 -0
- data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_prompt_response.rb +126 -0
- data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_resource_response.rb +121 -0
- data/lib/model_context_protocol/rspec/matchers/be_valid_mcp_tool_response.rb +135 -0
- data/lib/model_context_protocol/rspec/matchers/have_audio_content.rb +109 -0
- data/lib/model_context_protocol/rspec/matchers/have_embedded_resource_content.rb +150 -0
- data/lib/model_context_protocol/rspec/matchers/have_image_content.rb +109 -0
- data/lib/model_context_protocol/rspec/matchers/have_message_count.rb +87 -0
- data/lib/model_context_protocol/rspec/matchers/have_message_with_role.rb +152 -0
- data/lib/model_context_protocol/rspec/matchers/have_resource_annotations.rb +135 -0
- data/lib/model_context_protocol/rspec/matchers/have_resource_blob.rb +108 -0
- data/lib/model_context_protocol/rspec/matchers/have_resource_link_content.rb +138 -0
- data/lib/model_context_protocol/rspec/matchers/have_resource_mime_type.rb +103 -0
- data/lib/model_context_protocol/rspec/matchers/have_resource_text.rb +112 -0
- data/lib/model_context_protocol/rspec/matchers/have_structured_content.rb +88 -0
- data/lib/model_context_protocol/rspec/matchers/have_text_content.rb +113 -0
- data/lib/model_context_protocol/rspec/matchers.rb +31 -0
- data/lib/model_context_protocol/rspec.rb +23 -0
- data/lib/model_context_protocol/server/cancellable.rb +5 -5
- data/lib/model_context_protocol/server/{mcp_logger.rb → client_logger.rb} +8 -11
- data/lib/model_context_protocol/server/configuration.rb +196 -109
- data/lib/model_context_protocol/server/content_helpers.rb +1 -1
- data/lib/model_context_protocol/server/global_config/server_logging.rb +78 -0
- data/lib/model_context_protocol/server/progressable.rb +43 -21
- data/lib/model_context_protocol/server/prompt.rb +12 -21
- data/lib/model_context_protocol/server/redis_client_proxy.rb +2 -14
- data/lib/model_context_protocol/server/redis_config.rb +5 -7
- data/lib/model_context_protocol/server/redis_pool_manager.rb +11 -14
- data/lib/model_context_protocol/server/registry.rb +8 -0
- data/lib/model_context_protocol/server/resource.rb +7 -4
- data/lib/model_context_protocol/server/router.rb +285 -9
- data/lib/model_context_protocol/server/server_logger.rb +31 -0
- data/lib/model_context_protocol/server/stdio_configuration.rb +114 -0
- data/lib/model_context_protocol/server/stdio_transport/request_store.rb +12 -53
- data/lib/model_context_protocol/server/stdio_transport.rb +18 -12
- data/lib/model_context_protocol/server/streamable_http_configuration.rb +218 -0
- data/lib/model_context_protocol/server/streamable_http_transport/event_counter.rb +0 -13
- data/lib/model_context_protocol/server/streamable_http_transport/message_poller.rb +9 -9
- data/lib/model_context_protocol/server/streamable_http_transport/notification_queue.rb +0 -41
- data/lib/model_context_protocol/server/streamable_http_transport/request_store.rb +21 -124
- data/lib/model_context_protocol/server/streamable_http_transport/server_request_store.rb +167 -0
- data/lib/model_context_protocol/server/streamable_http_transport/session_message_queue.rb +0 -58
- data/lib/model_context_protocol/server/streamable_http_transport/session_store.rb +17 -31
- data/lib/model_context_protocol/server/streamable_http_transport/stream_registry.rb +0 -34
- data/lib/model_context_protocol/server/streamable_http_transport.rb +589 -215
- data/lib/model_context_protocol/server/tool.rb +73 -6
- data/lib/model_context_protocol/server.rb +204 -261
- data/lib/model_context_protocol/version.rb +1 -1
- data/lib/model_context_protocol.rb +4 -1
- data/lib/puma/plugin/mcp.rb +39 -0
- data/tasks/mcp.rake +26 -0
- data/tasks/templates/dev-http-puma.erb +251 -0
- data/tasks/templates/dev-http.erb +166 -184
- data/tasks/templates/dev.erb +29 -7
- metadata +33 -6
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module ModelContextProtocol
|
|
4
|
+
# Settings for servers that communicate via the MCP streamable HTTP transport, typically
|
|
5
|
+
# used by Rack applications serving multiple clients concurrently with Redis-backed coordination.
|
|
6
|
+
#
|
|
7
|
+
# Created by Server.with_streamable_http_transport, which yields an instance to a configuration
|
|
8
|
+
# block before passing it to Router. Adds session management (require_sessions), CORS control
|
|
9
|
+
# (validate_origin, allowed_origins), connection timeouts (session_ttl, ping_timeout), and
|
|
10
|
+
# Redis connection pool settings (redis_url, redis_pool_size, etc.) on top of the base class.
|
|
11
|
+
# validate_transport! verifies that redis_url is a valid Redis URL, and setup_transport!
|
|
12
|
+
# configures the Redis connection pool via RedisConfig before the server starts.
|
|
13
|
+
#
|
|
14
|
+
# Router queries supports_list_changed? (true for this subclass, false for stdio) to advertise
|
|
15
|
+
# listChanged capabilities in the initialize response. StreamableHttpTransport reads require_sessions,
|
|
16
|
+
# validate_origin, allowed_origins, session_ttl, and ping_timeout directly from this configuration.
|
|
17
|
+
class Server::StreamableHttpConfiguration < Server::Configuration
|
|
18
|
+
# @return [Symbol] :streamable_http (used by Server.start to instantiate StreamableHttpTransport)
|
|
19
|
+
def transport_type = :streamable_http
|
|
20
|
+
|
|
21
|
+
# @return [Boolean] true (Router advertises listChanged in initialize response capabilities;
|
|
22
|
+
# StreamableHttpTransport can push unsolicited notifications to clients)
|
|
23
|
+
def supports_list_changed? = true
|
|
24
|
+
|
|
25
|
+
# @!attribute [w] require_sessions
|
|
26
|
+
# Whether to enforce that clients send a session ID with each request.
|
|
27
|
+
# StreamableHttpTransport checks this and returns 400 Bad Request if session_id is missing.
|
|
28
|
+
# @see StreamableHttpTransport#handle
|
|
29
|
+
attr_writer :require_sessions
|
|
30
|
+
|
|
31
|
+
# @!attribute [w] validate_origin
|
|
32
|
+
# Whether to enforce CORS origin validation against allowed_origins.
|
|
33
|
+
# StreamableHttpTransport checks this in the OPTIONS preflight handler.
|
|
34
|
+
# @see StreamableHttpTransport#handle
|
|
35
|
+
attr_writer :validate_origin
|
|
36
|
+
|
|
37
|
+
# @!attribute [w] allowed_origins
|
|
38
|
+
# List of origins permitted in CORS requests; checked by StreamableHttpTransport
|
|
39
|
+
# when validate_origin is true. Supports exact strings or "*" wildcard.
|
|
40
|
+
# @see StreamableHttpTransport#handle
|
|
41
|
+
attr_writer :allowed_origins
|
|
42
|
+
|
|
43
|
+
# @!attribute [w] session_ttl
|
|
44
|
+
# How long (in seconds) Redis session entries persist after last activity.
|
|
45
|
+
# StreamableHttpTransport passes this to SessionStore.new to set key expiration.
|
|
46
|
+
# @see StreamableHttpTransport#initialize
|
|
47
|
+
attr_writer :session_ttl
|
|
48
|
+
|
|
49
|
+
# @!attribute [w] ping_timeout
|
|
50
|
+
# How long (in seconds) to wait for ping responses before considering a stream dead.
|
|
51
|
+
# StreamableHttpTransport passes this to StreamMonitor to detect stale connections.
|
|
52
|
+
# @see StreamableHttpTransport#initialize
|
|
53
|
+
attr_writer :ping_timeout
|
|
54
|
+
|
|
55
|
+
# @!attribute [w] redis_url
|
|
56
|
+
# The Redis connection URL (redis:// or rediss:// scheme). Required.
|
|
57
|
+
# Passed to RedisConfig during setup_transport! to create the connection pool.
|
|
58
|
+
attr_writer :redis_url
|
|
59
|
+
|
|
60
|
+
# @!attribute [w] redis_pool_size
|
|
61
|
+
# Number of Redis connections in the pool.
|
|
62
|
+
# Passed to RedisConfig during setup_transport!.
|
|
63
|
+
attr_writer :redis_pool_size
|
|
64
|
+
|
|
65
|
+
# @!attribute [w] redis_pool_timeout
|
|
66
|
+
# Seconds to wait for a connection from the pool before raising.
|
|
67
|
+
# Passed to RedisConfig during setup_transport!.
|
|
68
|
+
attr_writer :redis_pool_timeout
|
|
69
|
+
|
|
70
|
+
# @!attribute [w] redis_ssl_params
|
|
71
|
+
# SSL parameters for Redis connections (e.g., { verify_mode: OpenSSL::SSL::VERIFY_NONE }).
|
|
72
|
+
# Passed to RedisConfig during setup_transport!.
|
|
73
|
+
attr_writer :redis_ssl_params
|
|
74
|
+
|
|
75
|
+
# @!attribute [w] redis_enable_reaper
|
|
76
|
+
# Whether to enable the idle connection reaper thread.
|
|
77
|
+
# Passed to RedisConfig during setup_transport!.
|
|
78
|
+
attr_writer :redis_enable_reaper
|
|
79
|
+
|
|
80
|
+
# @!attribute [w] redis_reaper_interval
|
|
81
|
+
# How often (in seconds) the reaper checks for idle connections.
|
|
82
|
+
# Passed to RedisConfig during setup_transport!.
|
|
83
|
+
attr_writer :redis_reaper_interval
|
|
84
|
+
|
|
85
|
+
# @!attribute [w] redis_idle_timeout
|
|
86
|
+
# How long (in seconds) a connection can sit idle before the reaper closes it.
|
|
87
|
+
# Passed to RedisConfig during setup_transport!.
|
|
88
|
+
attr_writer :redis_idle_timeout
|
|
89
|
+
|
|
90
|
+
# Check whether session IDs are mandatory for incoming requests.
|
|
91
|
+
# StreamableHttpTransport reads this at request handling time to decide whether
|
|
92
|
+
# to reject requests without session_id query parameters.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean] true by default (sessions are required)
|
|
95
|
+
def require_sessions
|
|
96
|
+
@require_sessions.nil? ? true : @require_sessions
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check whether CORS origin validation is enforced.
|
|
100
|
+
# StreamableHttpTransport reads this during OPTIONS preflight handling to decide
|
|
101
|
+
# whether to reject requests with disallowed Origin headers.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] true by default (validate origins against allowed_origins list)
|
|
104
|
+
def validate_origin
|
|
105
|
+
@validate_origin.nil? ? true : @validate_origin
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Retrieve the list of permitted CORS origins.
|
|
109
|
+
# StreamableHttpTransport reads this during OPTIONS preflight handling to check
|
|
110
|
+
# the request's Origin header against allowed values.
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<String>] list of allowed origins (localhost on standard ports by default);
|
|
113
|
+
# supports exact matches or "*" for any origin
|
|
114
|
+
def allowed_origins
|
|
115
|
+
@allowed_origins || DEFAULT_ALLOWED_ORIGINS
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Retrieve the session expiration time in seconds.
|
|
119
|
+
# StreamableHttpTransport passes this to SessionStore.new, which sets Redis key TTLs
|
|
120
|
+
# to automatically expire inactive sessions.
|
|
121
|
+
#
|
|
122
|
+
# @return [Integer] seconds until session data expires (3600 = 1 hour by default)
|
|
123
|
+
def session_ttl
|
|
124
|
+
@session_ttl || 3600
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Retrieve the ping response timeout in seconds.
|
|
128
|
+
# StreamableHttpTransport passes this to StreamMonitor, which closes streams that
|
|
129
|
+
# don't respond to ping messages within this window (indicating client disconnection
|
|
130
|
+
# or network failure).
|
|
131
|
+
#
|
|
132
|
+
# @return [Integer] seconds to wait for ping responses (10 seconds by default)
|
|
133
|
+
def ping_timeout
|
|
134
|
+
@ping_timeout || 10
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [String, nil] the Redis connection URL
|
|
138
|
+
attr_reader :redis_url
|
|
139
|
+
|
|
140
|
+
# @return [Integer] number of Redis connections in the pool (default: 20)
|
|
141
|
+
def redis_pool_size
|
|
142
|
+
@redis_pool_size || 20
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @return [Integer] seconds to wait for a pool connection (default: 5)
|
|
146
|
+
def redis_pool_timeout
|
|
147
|
+
@redis_pool_timeout || 5
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @return [Hash, nil] SSL parameters for Redis connections
|
|
151
|
+
attr_reader :redis_ssl_params
|
|
152
|
+
|
|
153
|
+
# @return [Boolean] whether the idle connection reaper is enabled (default: true)
|
|
154
|
+
def redis_enable_reaper
|
|
155
|
+
@redis_enable_reaper.nil? ? true : @redis_enable_reaper
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @return [Integer] seconds between reaper checks (default: 60)
|
|
159
|
+
def redis_reaper_interval
|
|
160
|
+
@redis_reaper_interval || 60
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @return [Integer] seconds before idle connections are reaped (default: 300)
|
|
164
|
+
def redis_idle_timeout
|
|
165
|
+
@redis_idle_timeout || 300
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# Default CORS origins permitted for HTTP transport: localhost and 127.0.0.1 on both HTTP and HTTPS.
|
|
171
|
+
# StreamableHttpTransport uses this list when allowed_origins hasn't been explicitly set,
|
|
172
|
+
# allowing development and testing without additional configuration. Production deployments
|
|
173
|
+
# should override with actual client origins or ["*"] for unrestricted access.
|
|
174
|
+
#
|
|
175
|
+
# @return [Array<String>] four localhost variants with http/https and localhost/127.0.0.1
|
|
176
|
+
DEFAULT_ALLOWED_ORIGINS = [
|
|
177
|
+
"http://localhost", "https://localhost",
|
|
178
|
+
"http://127.0.0.1", "https://127.0.0.1"
|
|
179
|
+
].freeze
|
|
180
|
+
|
|
181
|
+
# Verify that redis_url is a valid Redis URL before allowing HTTP transport.
|
|
182
|
+
# Overrides the base class template method; called by validate! after checking name/version/registry.
|
|
183
|
+
# Uses URI parsing ("parse, don't validate") to catch nil, empty, malformed, and non-Redis URLs.
|
|
184
|
+
#
|
|
185
|
+
# @raise [InvalidTransportError] if redis_url is nil, empty, malformed, or not a redis:// / rediss:// URL
|
|
186
|
+
# @return [void]
|
|
187
|
+
def validate_transport!
|
|
188
|
+
uri = URI.parse(redis_url.to_s)
|
|
189
|
+
unless %w[redis rediss].include?(uri.scheme)
|
|
190
|
+
raise InvalidTransportError,
|
|
191
|
+
"streamable_http transport requires a valid Redis URL (redis:// or rediss://). " \
|
|
192
|
+
"Set config.redis_url in the configuration block."
|
|
193
|
+
end
|
|
194
|
+
rescue URI::InvalidURIError
|
|
195
|
+
raise InvalidTransportError,
|
|
196
|
+
"streamable_http transport requires a valid Redis URL (redis:// or rediss://). " \
|
|
197
|
+
"Set config.redis_url in the configuration block."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Configure the Redis connection pool via RedisConfig.
|
|
201
|
+
# Overrides the base class template method; called by Server.build_server after validate! passes.
|
|
202
|
+
# Maps the redis_* attributes on this configuration to the corresponding RedisConfig::Configuration
|
|
203
|
+
# attributes, then starts the pool manager.
|
|
204
|
+
#
|
|
205
|
+
# @return [void]
|
|
206
|
+
def setup_transport!
|
|
207
|
+
ModelContextProtocol::Server::RedisConfig.configure do |redis_config|
|
|
208
|
+
redis_config.redis_url = redis_url
|
|
209
|
+
redis_config.pool_size = redis_pool_size
|
|
210
|
+
redis_config.pool_timeout = redis_pool_timeout
|
|
211
|
+
redis_config.enable_reaper = redis_enable_reaper
|
|
212
|
+
redis_config.reaper_interval = redis_reaper_interval
|
|
213
|
+
redis_config.idle_timeout = redis_idle_timeout
|
|
214
|
+
redis_config.ssl_params = redis_ssl_params
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -17,19 +17,6 @@ module ModelContextProtocol
|
|
|
17
17
|
count = @redis.incr(@counter_key)
|
|
18
18
|
"#{@server_instance}-#{count}"
|
|
19
19
|
end
|
|
20
|
-
|
|
21
|
-
def current_count
|
|
22
|
-
count = @redis.get(@counter_key)
|
|
23
|
-
count ? count.to_i : 0
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def reset
|
|
27
|
-
@redis.set(@counter_key, 0)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def set_count(value)
|
|
31
|
-
@redis.set(@counter_key, value.to_i)
|
|
32
|
-
end
|
|
33
20
|
end
|
|
34
21
|
end
|
|
35
22
|
end
|
|
@@ -6,10 +6,10 @@ module ModelContextProtocol
|
|
|
6
6
|
POLL_INTERVAL = 0.1 # 100ms
|
|
7
7
|
BATCH_SIZE = 100
|
|
8
8
|
|
|
9
|
-
def initialize(redis_client, stream_registry,
|
|
9
|
+
def initialize(redis_client, stream_registry, client_logger, &message_delivery_block)
|
|
10
10
|
@redis = redis_client
|
|
11
11
|
@stream_registry = stream_registry
|
|
12
|
-
@
|
|
12
|
+
@client_logger = client_logger
|
|
13
13
|
@message_delivery_block = message_delivery_block
|
|
14
14
|
@running = false
|
|
15
15
|
@poll_thread = nil
|
|
@@ -22,14 +22,14 @@ module ModelContextProtocol
|
|
|
22
22
|
@poll_thread = Thread.new do
|
|
23
23
|
poll_loop
|
|
24
24
|
rescue => e
|
|
25
|
-
@
|
|
25
|
+
@client_logger.error("Message poller thread error", error: e.message, backtrace: e.backtrace.first(5))
|
|
26
26
|
sleep 1
|
|
27
27
|
retry if @running
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
@poll_thread.name = "MCP-MessagePoller" if @poll_thread.respond_to?(:name=)
|
|
31
31
|
|
|
32
|
-
@
|
|
32
|
+
@client_logger.debug("Message poller started")
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def stop
|
|
@@ -37,11 +37,11 @@ module ModelContextProtocol
|
|
|
37
37
|
|
|
38
38
|
if @poll_thread&.alive?
|
|
39
39
|
@poll_thread.kill
|
|
40
|
-
@poll_thread.join(
|
|
40
|
+
@poll_thread.join(5)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
@poll_thread = nil
|
|
44
|
-
@
|
|
44
|
+
@client_logger.debug("Message poller stopped")
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def running?
|
|
@@ -55,7 +55,7 @@ module ModelContextProtocol
|
|
|
55
55
|
begin
|
|
56
56
|
poll_and_deliver_messages
|
|
57
57
|
rescue => e
|
|
58
|
-
@
|
|
58
|
+
@client_logger.error("Error in message polling", error: e.message)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
sleep POLL_INTERVAL
|
|
@@ -91,9 +91,9 @@ module ModelContextProtocol
|
|
|
91
91
|
@message_delivery_block&.call(stream, message)
|
|
92
92
|
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
93
93
|
@stream_registry.unregister_stream(session_id)
|
|
94
|
-
@
|
|
94
|
+
@client_logger.debug("Unregistered disconnected stream", session_id: session_id)
|
|
95
95
|
rescue => e
|
|
96
|
-
@
|
|
96
|
+
@client_logger.error("Error delivering message to stream",
|
|
97
97
|
session_id: session_id, error: e.message)
|
|
98
98
|
end
|
|
99
99
|
end
|
|
@@ -22,13 +22,6 @@ module ModelContextProtocol
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def pop
|
|
26
|
-
notification_json = @redis.rpop(@queue_key)
|
|
27
|
-
return nil unless notification_json
|
|
28
|
-
|
|
29
|
-
JSON.parse(notification_json)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
25
|
def pop_all
|
|
33
26
|
notification_jsons = @redis.multi do |multi|
|
|
34
27
|
multi.lrange(@queue_key, 0, -1)
|
|
@@ -41,40 +34,6 @@ module ModelContextProtocol
|
|
|
41
34
|
JSON.parse(notification_json)
|
|
42
35
|
end
|
|
43
36
|
end
|
|
44
|
-
|
|
45
|
-
def peek_all
|
|
46
|
-
notification_jsons = @redis.lrange(@queue_key, 0, -1)
|
|
47
|
-
return [] if notification_jsons.empty?
|
|
48
|
-
|
|
49
|
-
notification_jsons.reverse.map do |notification_json|
|
|
50
|
-
JSON.parse(notification_json)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def size
|
|
55
|
-
@redis.llen(@queue_key)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def empty?
|
|
59
|
-
size == 0
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def clear
|
|
63
|
-
@redis.del(@queue_key)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def push_bulk(notifications)
|
|
67
|
-
return if notifications.empty?
|
|
68
|
-
|
|
69
|
-
notification_jsons = notifications.map(&:to_json)
|
|
70
|
-
|
|
71
|
-
@redis.multi do |multi|
|
|
72
|
-
notification_jsons.each do |json|
|
|
73
|
-
multi.lpush(@queue_key, json)
|
|
74
|
-
end
|
|
75
|
-
multi.ltrim(@queue_key, 0, @max_size - 1)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
37
|
end
|
|
79
38
|
end
|
|
80
39
|
end
|
|
@@ -19,10 +19,10 @@ module ModelContextProtocol
|
|
|
19
19
|
|
|
20
20
|
# Register a new request with its associated session
|
|
21
21
|
#
|
|
22
|
-
# @param
|
|
22
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
23
23
|
# @param session_id [String] the session identifier (can be nil for sessionless requests)
|
|
24
24
|
# @return [void]
|
|
25
|
-
def register_request(
|
|
25
|
+
def register_request(jsonrpc_request_id, session_id = nil)
|
|
26
26
|
request_data = {
|
|
27
27
|
session_id: session_id,
|
|
28
28
|
server_instance: @server_instance,
|
|
@@ -30,11 +30,11 @@ module ModelContextProtocol
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
@redis.multi do |multi|
|
|
33
|
-
multi.set("#{REQUEST_KEY_PREFIX}#{
|
|
33
|
+
multi.set("#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}",
|
|
34
34
|
request_data.to_json, ex: @ttl)
|
|
35
35
|
|
|
36
36
|
if session_id
|
|
37
|
-
multi.set("#{SESSION_KEY_PREFIX}#{session_id}:#{
|
|
37
|
+
multi.set("#{SESSION_KEY_PREFIX}#{session_id}:#{jsonrpc_request_id}",
|
|
38
38
|
true, ex: @ttl)
|
|
39
39
|
end
|
|
40
40
|
end
|
|
@@ -42,48 +42,37 @@ module ModelContextProtocol
|
|
|
42
42
|
|
|
43
43
|
# Mark a request as cancelled
|
|
44
44
|
#
|
|
45
|
-
# @param
|
|
45
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
46
46
|
# @param reason [String] optional reason for cancellation
|
|
47
47
|
# @return [Boolean] true if cancellation was recorded
|
|
48
|
-
def mark_cancelled(
|
|
48
|
+
def mark_cancelled(jsonrpc_request_id, reason = nil)
|
|
49
49
|
cancellation_data = {
|
|
50
50
|
cancelled_at: Time.now.to_f,
|
|
51
51
|
reason: reason
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
result = @redis.set("#{CANCELLED_KEY_PREFIX}#{
|
|
54
|
+
result = @redis.set("#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}",
|
|
55
55
|
cancellation_data.to_json, ex: @ttl)
|
|
56
56
|
result == "OK"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Check if a request has been cancelled
|
|
60
60
|
#
|
|
61
|
-
# @param
|
|
61
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
62
62
|
# @return [Boolean] true if the request is cancelled, false otherwise
|
|
63
|
-
def cancelled?(
|
|
64
|
-
@redis.exists("#{CANCELLED_KEY_PREFIX}#{
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Get cancellation information for a request
|
|
68
|
-
#
|
|
69
|
-
# @param request_id [String] the unique request identifier
|
|
70
|
-
# @return [Hash, nil] cancellation data or nil if not cancelled
|
|
71
|
-
def get_cancellation_info(request_id)
|
|
72
|
-
data = @redis.get("#{CANCELLED_KEY_PREFIX}#{request_id}")
|
|
73
|
-
data ? JSON.parse(data) : nil
|
|
74
|
-
rescue JSON::ParserError
|
|
75
|
-
nil
|
|
63
|
+
def cancelled?(jsonrpc_request_id)
|
|
64
|
+
@redis.exists("#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}") == 1
|
|
76
65
|
end
|
|
77
66
|
|
|
78
67
|
# Unregister a request (typically called when request completes)
|
|
79
68
|
#
|
|
80
|
-
# @param
|
|
69
|
+
# @param jsonrpc_request_id [String] the unique JSON-RPC request identifier
|
|
81
70
|
# @return [void]
|
|
82
|
-
def unregister_request(
|
|
83
|
-
request_data = @redis.get("#{REQUEST_KEY_PREFIX}#{
|
|
71
|
+
def unregister_request(jsonrpc_request_id)
|
|
72
|
+
request_data = @redis.get("#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}")
|
|
84
73
|
|
|
85
|
-
keys_to_delete = ["#{REQUEST_KEY_PREFIX}#{
|
|
86
|
-
"#{CANCELLED_KEY_PREFIX}#{
|
|
74
|
+
keys_to_delete = ["#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}",
|
|
75
|
+
"#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}"]
|
|
87
76
|
|
|
88
77
|
if request_data
|
|
89
78
|
begin
|
|
@@ -91,7 +80,7 @@ module ModelContextProtocol
|
|
|
91
80
|
session_id = data["session_id"]
|
|
92
81
|
|
|
93
82
|
if session_id
|
|
94
|
-
keys_to_delete << "#{SESSION_KEY_PREFIX}#{session_id}:#{
|
|
83
|
+
keys_to_delete << "#{SESSION_KEY_PREFIX}#{session_id}:#{jsonrpc_request_id}"
|
|
95
84
|
end
|
|
96
85
|
rescue JSON::ParserError
|
|
97
86
|
nil
|
|
@@ -101,25 +90,6 @@ module ModelContextProtocol
|
|
|
101
90
|
@redis.del(*keys_to_delete) unless keys_to_delete.empty?
|
|
102
91
|
end
|
|
103
92
|
|
|
104
|
-
# Get information about a specific request
|
|
105
|
-
#
|
|
106
|
-
# @param request_id [String] the unique request identifier
|
|
107
|
-
# @return [Hash, nil] request information or nil if not found
|
|
108
|
-
def get_request(request_id)
|
|
109
|
-
data = @redis.get("#{REQUEST_KEY_PREFIX}#{request_id}")
|
|
110
|
-
data ? JSON.parse(data) : nil
|
|
111
|
-
rescue JSON::ParserError
|
|
112
|
-
nil
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Check if a request is currently active
|
|
116
|
-
#
|
|
117
|
-
# @param request_id [String] the unique request identifier
|
|
118
|
-
# @return [Boolean] true if the request is active, false otherwise
|
|
119
|
-
def active?(request_id)
|
|
120
|
-
@redis.exists("#{REQUEST_KEY_PREFIX}#{request_id}") == 1
|
|
121
|
-
end
|
|
122
|
-
|
|
123
93
|
# Clean up all requests associated with a session
|
|
124
94
|
# This is typically called when a session is terminated
|
|
125
95
|
#
|
|
@@ -131,93 +101,20 @@ module ModelContextProtocol
|
|
|
131
101
|
return [] if request_keys.empty?
|
|
132
102
|
|
|
133
103
|
# Extract request IDs from the keys
|
|
134
|
-
|
|
104
|
+
jsonrpc_request_ids = request_keys.map do |key|
|
|
135
105
|
key.sub("#{SESSION_KEY_PREFIX}#{session_id}:", "")
|
|
136
106
|
end
|
|
137
107
|
|
|
138
108
|
# Delete all related keys
|
|
139
109
|
all_keys = []
|
|
140
|
-
|
|
141
|
-
all_keys << "#{REQUEST_KEY_PREFIX}#{
|
|
142
|
-
all_keys << "#{CANCELLED_KEY_PREFIX}#{
|
|
110
|
+
jsonrpc_request_ids.each do |jsonrpc_request_id|
|
|
111
|
+
all_keys << "#{REQUEST_KEY_PREFIX}#{jsonrpc_request_id}"
|
|
112
|
+
all_keys << "#{CANCELLED_KEY_PREFIX}#{jsonrpc_request_id}"
|
|
143
113
|
end
|
|
144
114
|
all_keys.concat(request_keys)
|
|
145
115
|
|
|
146
116
|
@redis.del(*all_keys) unless all_keys.empty?
|
|
147
|
-
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Get all active request IDs for a specific session
|
|
151
|
-
#
|
|
152
|
-
# @param session_id [String] the session identifier
|
|
153
|
-
# @return [Array<String>] list of active request IDs for the session
|
|
154
|
-
def get_session_requests(session_id)
|
|
155
|
-
pattern = "#{SESSION_KEY_PREFIX}#{session_id}:*"
|
|
156
|
-
request_keys = @redis.keys(pattern)
|
|
157
|
-
|
|
158
|
-
request_keys.map do |key|
|
|
159
|
-
key.sub("#{SESSION_KEY_PREFIX}#{session_id}:", "")
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# Get all active request IDs across all sessions
|
|
164
|
-
#
|
|
165
|
-
# @return [Array<String>] list of all active request IDs
|
|
166
|
-
def get_all_active_requests
|
|
167
|
-
pattern = "#{REQUEST_KEY_PREFIX}*"
|
|
168
|
-
request_keys = @redis.keys(pattern)
|
|
169
|
-
|
|
170
|
-
request_keys.map do |key|
|
|
171
|
-
key.sub(REQUEST_KEY_PREFIX, "")
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Clean up expired requests based on TTL
|
|
176
|
-
# This method can be called periodically to ensure cleanup
|
|
177
|
-
#
|
|
178
|
-
# @return [Integer] number of expired requests cleaned up
|
|
179
|
-
def cleanup_expired_requests
|
|
180
|
-
active_keys = @redis.keys("#{REQUEST_KEY_PREFIX}*")
|
|
181
|
-
expired_count = 0
|
|
182
|
-
key_exists_without_expiration = -1
|
|
183
|
-
key_does_not_exist = -2
|
|
184
|
-
|
|
185
|
-
active_keys.each do |key|
|
|
186
|
-
ttl = @redis.ttl(key)
|
|
187
|
-
if ttl == key_exists_without_expiration
|
|
188
|
-
@redis.expire(key, @ttl)
|
|
189
|
-
elsif ttl == key_does_not_exist
|
|
190
|
-
expired_count += 1
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
expired_count
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Refresh the TTL for an active request
|
|
198
|
-
#
|
|
199
|
-
# @param request_id [String] the unique request identifier
|
|
200
|
-
# @return [Boolean] true if TTL was refreshed, false if request doesn't exist
|
|
201
|
-
def refresh_request_ttl(request_id)
|
|
202
|
-
request_data = @redis.get("#{REQUEST_KEY_PREFIX}#{request_id}")
|
|
203
|
-
return false unless request_data
|
|
204
|
-
|
|
205
|
-
@redis.multi do |multi|
|
|
206
|
-
multi.expire("#{REQUEST_KEY_PREFIX}#{request_id}", @ttl)
|
|
207
|
-
multi.expire("#{CANCELLED_KEY_PREFIX}#{request_id}", @ttl)
|
|
208
|
-
|
|
209
|
-
begin
|
|
210
|
-
data = JSON.parse(request_data)
|
|
211
|
-
session_id = data["session_id"]
|
|
212
|
-
if session_id
|
|
213
|
-
multi.expire("#{SESSION_KEY_PREFIX}#{session_id}:#{request_id}", @ttl)
|
|
214
|
-
end
|
|
215
|
-
rescue JSON::ParserError
|
|
216
|
-
nil
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
true
|
|
117
|
+
jsonrpc_request_ids
|
|
221
118
|
end
|
|
222
119
|
end
|
|
223
120
|
end
|