ruby-mcp-client 2.1.0 → 3.0.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/OAUTH.md +555 -0
- data/README.md +825 -48
- data/lib/mcp_client/audio_content.rb +1 -1
- data/lib/mcp_client/auth/browser_oauth.rb +131 -21
- data/lib/mcp_client/auth/oauth_provider/challenge_handling.rb +532 -0
- data/lib/mcp_client/auth/oauth_provider/client_authentication.rb +121 -0
- data/lib/mcp_client/auth/oauth_provider/pending_requests.rb +51 -0
- data/lib/mcp_client/auth/oauth_provider/registration_store.rb +486 -0
- data/lib/mcp_client/auth/oauth_provider/response_validation.rb +441 -0
- data/lib/mcp_client/auth/oauth_provider/scope_selection.rb +134 -0
- data/lib/mcp_client/auth/oauth_provider/token_store.rb +419 -0
- data/lib/mcp_client/auth/oauth_provider.rb +1354 -386
- data/lib/mcp_client/auth/peer_text.rb +174 -0
- data/lib/mcp_client/auth.rb +298 -32
- data/lib/mcp_client/cached_result.rb +145 -0
- data/lib/mcp_client/called_tool_definition.rb +138 -0
- data/lib/mcp_client/client/cache_slices.rb +195 -0
- data/lib/mcp_client/client/list_aggregation.rb +243 -0
- data/lib/mcp_client/client/notification_routing.rb +155 -0
- data/lib/mcp_client/client/sampling_validation.rb +200 -0
- data/lib/mcp_client/client/task_api.rb +531 -0
- data/lib/mcp_client/client/task_lifetimes.rb +269 -0
- data/lib/mcp_client/client/task_registry.rb +254 -0
- data/lib/mcp_client/client/task_shape.rb +102 -0
- data/lib/mcp_client/client/task_support.rb +1166 -0
- data/lib/mcp_client/client/task_updates.rb +457 -0
- data/lib/mcp_client/client/task_wait_boundaries.rb +198 -0
- data/lib/mcp_client/client/task_workers.rb +63 -0
- data/lib/mcp_client/client.rb +796 -518
- data/lib/mcp_client/deep_copy.rb +49 -0
- data/lib/mcp_client/deprecation_notices.rb +94 -0
- data/lib/mcp_client/deprecations.rb +419 -0
- data/lib/mcp_client/errors.rb +474 -7
- data/lib/mcp_client/header_params.rb +320 -0
- data/lib/mcp_client/http_transport_base/bounded_inflate.rb +41 -0
- data/lib/mcp_client/http_transport_base/cache_support.rb +694 -0
- data/lib/mcp_client/http_transport_base/era_detection.rb +134 -0
- data/lib/mcp_client/http_transport_base/listen_stream.rb +763 -0
- data/lib/mcp_client/http_transport_base/param_headers.rb +35 -0
- data/lib/mcp_client/http_transport_base/request_recovery.rb +156 -0
- data/lib/mcp_client/http_transport_base/session_recovery.rb +113 -0
- data/lib/mcp_client/http_transport_base/sse_event_scanner.rb +145 -0
- data/lib/mcp_client/http_transport_base/stream_capture.rb +160 -0
- data/lib/mcp_client/http_transport_base/stream_recovery.rb +318 -0
- data/lib/mcp_client/http_transport_base/tool_listing.rb +277 -0
- data/lib/mcp_client/http_transport_base.rb +666 -120
- data/lib/mcp_client/input_round_trips.rb +128 -0
- data/lib/mcp_client/json_rpc_common/envelopes.rb +32 -0
- data/lib/mcp_client/json_rpc_common/error_bodies.rb +105 -0
- data/lib/mcp_client/json_rpc_common/input_waits.rb +167 -0
- data/lib/mcp_client/json_rpc_common.rb +900 -13
- data/lib/mcp_client/oauth_client.rb +14 -5
- data/lib/mcp_client/prompt.rb +4 -0
- data/lib/mcp_client/request_authorization.rb +128 -0
- data/lib/mcp_client/request_meta_scope.rb +77 -0
- data/lib/mcp_client/request_metadata.rb +287 -0
- data/lib/mcp_client/resource.rb +4 -0
- data/lib/mcp_client/resource_content.rb +20 -0
- data/lib/mcp_client/resource_template.rb +4 -0
- data/lib/mcp_client/result_caching.rb +999 -0
- data/lib/mcp_client/result_completeness.rb +34 -0
- data/lib/mcp_client/root.rb +6 -0
- data/lib/mcp_client/round_trip_marker.rb +28 -0
- data/lib/mcp_client/schema_validator/annotations.rb +82 -0
- data/lib/mcp_client/schema_validator/composition.rb +86 -0
- data/lib/mcp_client/schema_validator/dialects.rb +66 -0
- data/lib/mcp_client/schema_validator/ecma_patterns.rb +567 -0
- data/lib/mcp_client/schema_validator/evaluation.rb +517 -0
- data/lib/mcp_client/schema_validator/input_requirements.rb +84 -0
- data/lib/mcp_client/schema_validator/instances.rb +449 -0
- data/lib/mcp_client/schema_validator/keyword_scan.rb +121 -0
- data/lib/mcp_client/schema_validator/normalization.rb +104 -0
- data/lib/mcp_client/schema_validator/references.rb +610 -0
- data/lib/mcp_client/schema_validator/scalars.rb +126 -0
- data/lib/mcp_client/schema_validator/shapes.rb +319 -0
- data/lib/mcp_client/schema_validator/uri_references.rb +153 -0
- data/lib/mcp_client/schema_validator.rb +882 -208
- data/lib/mcp_client/server_base.rb +233 -5
- data/lib/mcp_client/server_factory.rb +9 -3
- data/lib/mcp_client/server_http/json_rpc_transport.rb +219 -4
- data/lib/mcp_client/server_http.rb +307 -90
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +113 -25
- data/lib/mcp_client/server_sse/sse_parser.rb +39 -6
- data/lib/mcp_client/server_sse.rb +227 -62
- data/lib/mcp_client/server_stdio/child_session.rb +98 -0
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +1003 -28
- data/lib/mcp_client/server_stdio.rb +772 -183
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +189 -25
- data/lib/mcp_client/server_streamable_http.rb +302 -115
- data/lib/mcp_client/session_pin.rb +119 -0
- data/lib/mcp_client/subscription/notification_dispatcher.rb +354 -0
- data/lib/mcp_client/subscription.rb +852 -0
- data/lib/mcp_client/subscription_support.rb +715 -0
- data/lib/mcp_client/task.rb +286 -14
- data/lib/mcp_client/tool.rb +31 -3
- data/lib/mcp_client/version.rb +21 -6
- data/lib/mcp_client.rb +108 -19
- metadata +68 -2
|
@@ -11,6 +11,14 @@ module MCPClient
|
|
|
11
11
|
include OriginPolicy
|
|
12
12
|
include JsonRpcCommon
|
|
13
13
|
|
|
14
|
+
# Returned by #check_for_result when nothing has arrived for the
|
|
15
|
+
# request yet. The stored result is whatever `result` member the
|
|
16
|
+
# response carried, so `null` and `false` are answers the client must
|
|
17
|
+
# deliver (and validate) rather than truthiness the waiter can read as
|
|
18
|
+
# "still outstanding" — doing that waited out the whole read timeout
|
|
19
|
+
# and cancelled a request the server had already answered.
|
|
20
|
+
NO_RESULT = Object.new.freeze
|
|
21
|
+
|
|
14
22
|
# Generic JSON-RPC request: send method with params and return result
|
|
15
23
|
# @param method [String] JSON-RPC method name
|
|
16
24
|
# @param params [Hash] parameters for the request
|
|
@@ -22,20 +30,31 @@ module MCPClient
|
|
|
22
30
|
def rpc_request(method, params = {}, timeout: nil)
|
|
23
31
|
ensure_initialized
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# MCP lifecycle: on timeout the sender SHOULD issue a cancellation
|
|
32
|
-
# notification for the abandoned request and stop waiting.
|
|
33
|
-
send_cancellation_notification(request_id) if cancellable_request?(method, params)
|
|
34
|
-
raise
|
|
35
|
-
end
|
|
33
|
+
# The multi round-trip resolver sits outside the per-attempt retry, so
|
|
34
|
+
# a retry carrying inputResponses/requestState keeps them through
|
|
35
|
+
# transport retries — the same shape as the other transports, so an
|
|
36
|
+
# input_required answer is validated and fulfilled here as well.
|
|
37
|
+
resolve_input_round_trips(method, params, timeout) do |attempt_params|
|
|
38
|
+
with_retry(method) { send_one_request(method, attempt_params, timeout) }
|
|
36
39
|
end
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
# One request on the wire: build it, send it and wait for its answer.
|
|
43
|
+
# @param method [String] JSON-RPC method
|
|
44
|
+
# @param params [Hash] the params of this attempt
|
|
45
|
+
# @param timeout [Numeric, nil] per-request timeout
|
|
46
|
+
# @return [Object] the result
|
|
47
|
+
def send_one_request(method, params, timeout)
|
|
48
|
+
request_id = @mutex.synchronize { @request_id += 1 }
|
|
49
|
+
request = build_jsonrpc_request(method, params, request_id)
|
|
50
|
+
send_jsonrpc_request(request, timeout: timeout)
|
|
51
|
+
rescue MCPClient::Errors::RequestTimeoutError
|
|
52
|
+
# MCP lifecycle: on timeout the sender SHOULD issue a cancellation
|
|
53
|
+
# notification for the abandoned request and stop waiting.
|
|
54
|
+
send_cancellation_notification(request_id) if request_id && cancellable_request?(method, params)
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
57
|
+
|
|
39
58
|
# Best-effort notifications/cancelled for a request the client stopped
|
|
40
59
|
# waiting on. Failures are swallowed.
|
|
41
60
|
# @param request_id [Integer] id of the abandoned request
|
|
@@ -89,7 +108,15 @@ module MCPClient
|
|
|
89
108
|
request_id = @mutex.synchronize { @request_id += 1 }
|
|
90
109
|
json_rpc_request = build_jsonrpc_request('initialize', initialization_params, request_id)
|
|
91
110
|
@logger.debug("Performing initialize RPC: #{json_rpc_request}")
|
|
92
|
-
|
|
111
|
+
begin
|
|
112
|
+
result = send_jsonrpc_request(json_rpc_request)
|
|
113
|
+
rescue MCPClient::Errors::UnsupportedProtocolVersionError => e
|
|
114
|
+
# As on the HTTP transports: the versions a modern-only server
|
|
115
|
+
# names in `data` are the diagnostic a legacy configuration needs,
|
|
116
|
+
# so they are spelled out rather than dropped by connect's wrap.
|
|
117
|
+
raise MCPClient::Errors::ConnectionError,
|
|
118
|
+
"Initialize failed: #{e.message} (server supports: #{e.supported.join(', ')})"
|
|
119
|
+
end
|
|
93
120
|
unless result.is_a?(Hash)
|
|
94
121
|
# A non-object initialize result means the handshake did not succeed.
|
|
95
122
|
# Continuing would enter the Operation phase without ever sending the
|
|
@@ -122,6 +149,10 @@ module MCPClient
|
|
|
122
149
|
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
|
123
150
|
# @raise [MCPClient::Errors::ToolCallError] for other errors during request execution
|
|
124
151
|
def send_jsonrpc_request(request, timeout: nil)
|
|
152
|
+
# As late as a request pinned to a session can be held back: every
|
|
153
|
+
# reconnect on the way here (ensure_initialized, a retry after the
|
|
154
|
+
# stream dropped) has happened by now.
|
|
155
|
+
check_session_pin!
|
|
125
156
|
@logger.debug("Sending JSON-RPC request: #{describe_jsonrpc_message(request)}")
|
|
126
157
|
record_activity
|
|
127
158
|
# Register the id BEFORE posting: the SSE stream may deliver the
|
|
@@ -130,14 +161,23 @@ module MCPClient
|
|
|
130
161
|
register_pending_request(request['id'])
|
|
131
162
|
|
|
132
163
|
begin
|
|
164
|
+
clear_response_received_at if respond_to?(:clear_response_received_at, true)
|
|
133
165
|
response = post_json_rpc_request(request)
|
|
134
166
|
|
|
135
167
|
if @use_sse
|
|
168
|
+
# Dated by check_for_result from the stream's arrival time.
|
|
136
169
|
wait_for_sse_result(request, timeout: timeout)
|
|
137
170
|
else
|
|
171
|
+
# Dated from receipt, before the body is decoded.
|
|
172
|
+
note_response_received_at if respond_to?(:note_response_received_at, true)
|
|
138
173
|
parse_direct_response(response)
|
|
139
174
|
end
|
|
140
|
-
|
|
175
|
+
# A pre-write refusal keeps its type: the late pin check inside the
|
|
176
|
+
# POST turns a request down (or the caller's own guard does, see
|
|
177
|
+
# {MCPClient::SessionPin#guarded_writes}) and nothing was written,
|
|
178
|
+
# which is not an error of executing the request.
|
|
179
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError,
|
|
180
|
+
MCPClient::Errors::ServerError, MCPClient::Errors::TaskReplacedError
|
|
141
181
|
raise
|
|
142
182
|
rescue JSON::ParserError => e
|
|
143
183
|
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{describe_parse_error(e)}"
|
|
@@ -167,6 +207,7 @@ module MCPClient
|
|
|
167
207
|
@mutex.synchronize do
|
|
168
208
|
@pending_request_ids.delete(request_id)
|
|
169
209
|
@sse_results.delete(request_id)
|
|
210
|
+
@sse_result_arrivals&.delete(request_id)
|
|
170
211
|
end
|
|
171
212
|
end
|
|
172
213
|
|
|
@@ -177,7 +218,15 @@ module MCPClient
|
|
|
177
218
|
def post_json_rpc_request(request)
|
|
178
219
|
uri = URI.parse(@base_url)
|
|
179
220
|
base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
180
|
-
|
|
221
|
+
# The endpoint this request goes to, and the pin checked in the same
|
|
222
|
+
# critical section: a reconnect completing between the check in
|
|
223
|
+
# #send_jsonrpc_request and this capture would otherwise post a
|
|
224
|
+
# request of the ended session to the session that replaced it
|
|
225
|
+
# (cleanup bumps the epoch before it takes this monitor).
|
|
226
|
+
rpc_ep = @mutex.synchronize do
|
|
227
|
+
check_session_pin!
|
|
228
|
+
@rpc_endpoint
|
|
229
|
+
end
|
|
181
230
|
|
|
182
231
|
@rpc_conn ||= create_json_rpc_connection(base)
|
|
183
232
|
|
|
@@ -187,9 +236,13 @@ module MCPClient
|
|
|
187
236
|
|
|
188
237
|
unless response.success?
|
|
189
238
|
# 5xx failures are plausibly transient (retryable); 4xx and other
|
|
190
|
-
# statuses are deterministic and raise a plain (non-retryable)
|
|
191
|
-
|
|
192
|
-
|
|
239
|
+
# statuses are deterministic and raise a plain (non-retryable)
|
|
240
|
+
# error — typed when the body carries a JSON-RPC error (MCP
|
|
241
|
+
# 2026-07-28 protocol errors ride in 400/404 bodies).
|
|
242
|
+
message = "Server returned error: #{response.status} #{response.reason_phrase}"
|
|
243
|
+
raise MCPClient::Errors::TransientServerError, message if (500..599).cover?(response.status)
|
|
244
|
+
|
|
245
|
+
raise jsonrpc_error_from_http_response(response, message)
|
|
193
246
|
end
|
|
194
247
|
|
|
195
248
|
response
|
|
@@ -231,8 +284,14 @@ module MCPClient
|
|
|
231
284
|
h.delete('Accept')
|
|
232
285
|
h.delete('Cache-Control')
|
|
233
286
|
end).each { |k, v| req.headers[k] = v }
|
|
287
|
+
# MCP 2026-07-28 caching: the result is bound to the credentials
|
|
288
|
+
# this very request carries.
|
|
289
|
+
if respond_to?(:note_request_authorization, true)
|
|
290
|
+
note_request_authorization(authorization_header_value(req.headers))
|
|
291
|
+
end
|
|
234
292
|
req.body = request.to_json
|
|
235
293
|
end
|
|
294
|
+
note_sent_authorization(response) if respond_to?(:note_sent_authorization, true)
|
|
236
295
|
|
|
237
296
|
msg = "Received JSON-RPC response: #{response.status}"
|
|
238
297
|
msg += " (#{describe_body_size(response.body)})" if response.respond_to?(:body)
|
|
@@ -276,7 +335,7 @@ module MCPClient
|
|
|
276
335
|
def wait_for_result_with_timeout(request_id, start_time, timeout)
|
|
277
336
|
loop do
|
|
278
337
|
result = check_for_result(request_id)
|
|
279
|
-
return result
|
|
338
|
+
return result unless result.equal?(NO_RESULT)
|
|
280
339
|
|
|
281
340
|
unless connection_active?
|
|
282
341
|
raise MCPClient::Errors::ConnectionError,
|
|
@@ -294,35 +353,64 @@ module MCPClient
|
|
|
294
353
|
|
|
295
354
|
# Check if a result is available for the given request ID
|
|
296
355
|
# @param request_id [Integer] the request ID to check
|
|
297
|
-
# @return [
|
|
356
|
+
# @return [Object] the result if one has arrived, NO_RESULT otherwise
|
|
298
357
|
# @raise [MCPClient::Errors::ServerError] if the stored result is a JSON-RPC error response
|
|
299
358
|
def check_for_result(request_id)
|
|
359
|
+
arrived = false
|
|
300
360
|
result = nil
|
|
361
|
+
arrival = nil
|
|
301
362
|
@mutex.synchronize do
|
|
302
|
-
|
|
363
|
+
if @sse_results.key?(request_id)
|
|
364
|
+
arrived = true
|
|
365
|
+
result = @sse_results.delete(request_id)
|
|
366
|
+
end
|
|
367
|
+
arrival = @sse_result_arrivals&.delete(request_id)
|
|
303
368
|
end
|
|
369
|
+
return NO_RESULT unless arrived
|
|
304
370
|
|
|
305
|
-
if
|
|
371
|
+
if arrived
|
|
306
372
|
record_activity
|
|
373
|
+
note_response_received_at(arrival || monotonic_now) if respond_to?(:note_response_received_at, true)
|
|
307
374
|
# SseParser#process_response? stores JSON-RPC error responses under
|
|
308
375
|
# the Symbol :error key; deliver them to the caller as ServerError
|
|
309
376
|
# (MCP lifecycle "Error Handling") instead of timing out.
|
|
310
377
|
raise_sse_error_response(result[:error]) if result.is_a?(Hash) && result.key?(:error)
|
|
378
|
+
# …and an envelope that carried neither member under :no_answer.
|
|
379
|
+
if result.is_a?(Hash) && result[:no_answer]
|
|
380
|
+
raise MCPClient::Errors::InvalidResultError,
|
|
381
|
+
"Invalid result: the response to request #{request_id} carried neither a result nor an error member"
|
|
382
|
+
end
|
|
383
|
+
# Same resultType invariant as process_jsonrpc_response on the
|
|
384
|
+
# other transports: an unrecognized value is an invalid response.
|
|
385
|
+
validate_result_type!(result)
|
|
311
386
|
return result
|
|
312
387
|
end
|
|
313
388
|
|
|
314
389
|
nil
|
|
315
390
|
end
|
|
316
391
|
|
|
392
|
+
# The legacy HTTP+SSE transport never negotiates a modern revision, so
|
|
393
|
+
# subscriptions/listen (which requires one) is refused by the shared
|
|
394
|
+
# implementation after this readiness check.
|
|
395
|
+
# @return [void]
|
|
396
|
+
def ensure_session_ready
|
|
397
|
+
ensure_connected if respond_to?(:ensure_connected, true)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# @param _subscription [MCPClient::Subscription]
|
|
401
|
+
# @return [void]
|
|
402
|
+
def cancel_subscription(_subscription)
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
|
|
317
406
|
# Raise a ServerError for a JSON-RPC error response received over SSE,
|
|
318
407
|
# mirroring JsonRpcCommon#process_jsonrpc_response for the other transports.
|
|
319
408
|
# @param error [Hash, nil] the JSON-RPC error object ('code', 'message', 'data')
|
|
320
409
|
# @raise [MCPClient::Errors::ServerError] always
|
|
321
410
|
def raise_sse_error_response(error)
|
|
322
|
-
|
|
323
|
-
message =
|
|
324
|
-
|
|
325
|
-
raise MCPClient::Errors::ServerError, message
|
|
411
|
+
typed = MCPClient::Errors::ServerError.from_jsonrpc(error)
|
|
412
|
+
message = typed.code ? "#{typed.message} (code #{typed.code})" : typed.message
|
|
413
|
+
raise typed.class.new(message, code: typed.code, data: typed.data)
|
|
326
414
|
end
|
|
327
415
|
|
|
328
416
|
# Parse a direct (non-SSE) JSON-RPC response
|
|
@@ -12,7 +12,8 @@ module MCPClient
|
|
|
12
12
|
|
|
13
13
|
# Parse and handle a raw SSE event payload.
|
|
14
14
|
# @param event_data [String] the raw event chunk
|
|
15
|
-
|
|
15
|
+
# @param arrived [Float, nil] monotonic time the chunk carrying it arrived
|
|
16
|
+
def parse_and_handle_sse_event(event_data, arrived = nil)
|
|
16
17
|
event = parse_sse_event(event_data)
|
|
17
18
|
return if event.nil?
|
|
18
19
|
|
|
@@ -22,23 +23,27 @@ module MCPClient
|
|
|
22
23
|
when 'ping'
|
|
23
24
|
# no-op
|
|
24
25
|
when 'message'
|
|
25
|
-
handle_message_event(event)
|
|
26
|
+
handle_message_event(event, arrived)
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# Handle a "message" SSE event (payload is JSON-RPC over SSE)
|
|
30
31
|
# @param event [Hash] the parsed SSE event (with :data, :id, etc)
|
|
31
|
-
|
|
32
|
+
# @param arrived [Float, nil] monotonic time the chunk carrying it arrived
|
|
33
|
+
def handle_message_event(event, arrived = nil)
|
|
32
34
|
return if event[:data].empty?
|
|
33
35
|
|
|
34
36
|
begin
|
|
37
|
+
# Dated from the arrival of the chunk it came in, before any event
|
|
38
|
+
# of that chunk was decoded or dispatched.
|
|
39
|
+
arrived ||= monotonic_now if respond_to?(:monotonic_now, true)
|
|
35
40
|
data = JSON.parse(event[:data])
|
|
36
41
|
|
|
37
42
|
return if process_error_in_message?(data)
|
|
38
43
|
return if process_server_request?(data)
|
|
39
44
|
return if process_notification?(data)
|
|
40
45
|
|
|
41
|
-
process_response?(data)
|
|
46
|
+
process_response?(data, arrived)
|
|
42
47
|
rescue MCPClient::Errors::ConnectionError
|
|
43
48
|
raise
|
|
44
49
|
rescue JSON::ParserError => e
|
|
@@ -85,14 +90,32 @@ module MCPClient
|
|
|
85
90
|
def process_notification?(data)
|
|
86
91
|
return false unless data['method'] && !data.key?('id')
|
|
87
92
|
|
|
93
|
+
# notifications/message is the Logging utility, Deprecated as a whole
|
|
94
|
+
# in 2026-07-28 (SEP-2577). The notice belongs to the transport, not
|
|
95
|
+
# to MCPClient::Client: a host that registered on_notification on a
|
|
96
|
+
# ServerSSE receives log messages without a Client ever existing. It
|
|
97
|
+
# is raised here rather than only in
|
|
98
|
+
# {MCPClient::SubscriptionSupport#route_notification} because this
|
|
99
|
+
# transport is the one that does not go through it — see below.
|
|
100
|
+
warn_logging_deprecated if data['method'] == 'notifications/message'
|
|
101
|
+
# The legacy SSE transport carries no subscriptions/listen stream, so
|
|
102
|
+
# there is no delivery to run ahead of — but the transport's own caches
|
|
103
|
+
# and a host that registered its invalidation on the dedicated hook
|
|
104
|
+
# must still be told, in the order routing uses them
|
|
105
|
+
# (see {MCPClient::ServerBase#on_cache_invalidation}).
|
|
106
|
+
invalidate_cache_for_notification(data['method'], data['params']) if respond_to?(
|
|
107
|
+
:invalidate_cache_for_notification, true
|
|
108
|
+
)
|
|
109
|
+
notify_cache_invalidation(data['method'], data['params'])
|
|
88
110
|
@notification_callback&.call(data['method'], data['params'])
|
|
89
111
|
true
|
|
90
112
|
end
|
|
91
113
|
|
|
92
114
|
# Process a JSON-RPC response (id => response)
|
|
93
115
|
# @param data [Hash] the parsed JSON payload
|
|
116
|
+
# @param arrived [Float, nil] monotonic time the event arrived
|
|
94
117
|
# @return [Boolean] true if we saw & handled a response
|
|
95
|
-
def process_response?(data)
|
|
118
|
+
def process_response?(data, arrived = nil)
|
|
96
119
|
return false unless data['id']
|
|
97
120
|
|
|
98
121
|
# Deliver the response to the waiting caller via @sse_results only.
|
|
@@ -110,14 +133,24 @@ module MCPClient
|
|
|
110
133
|
return true
|
|
111
134
|
end
|
|
112
135
|
|
|
136
|
+
# Dated from arrival: the waiter polls and may wake much later.
|
|
137
|
+
(@sse_result_arrivals ||= {})[data['id']] = arrived || monotonic_now if respond_to?(:monotonic_now, true)
|
|
113
138
|
@sse_results[data['id']] =
|
|
114
139
|
if data['error']
|
|
115
140
|
# JSON-RPC error response: store the error under a Symbol key
|
|
116
141
|
# (JSON.parse only produces String keys, so this cannot collide
|
|
117
142
|
# with a success result) for the waiter to raise ServerError.
|
|
118
143
|
{ error: data['error'] }
|
|
119
|
-
|
|
144
|
+
elsif data.key?('result')
|
|
145
|
+
# An explicit null (or false) result is an answer; only the
|
|
146
|
+
# member's presence decides that.
|
|
120
147
|
data['result']
|
|
148
|
+
else
|
|
149
|
+
# JSON-RPC 2.0 section 5: "Either the result member or error
|
|
150
|
+
# member MUST be included". An envelope carrying neither answers
|
|
151
|
+
# nothing, and reading its absent result as a delivered nil
|
|
152
|
+
# would turn a malformed response into a successful call.
|
|
153
|
+
{ no_answer: true }
|
|
121
154
|
end
|
|
122
155
|
end
|
|
123
156
|
|