mcp 1.8.1 → 1.8.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 255e316335616783063d8a7374f385ff10b99c9ddef24571734e19b6ec6f5f93
4
- data.tar.gz: 07021e227f5b1be05fd2c1e4dc88e0d563bc963b6f1190f998aa632ed6871031
3
+ metadata.gz: e99b2f4d4d5727123d1ae167586134b1f06498f975ed2b16cb2f7e1c413d1271
4
+ data.tar.gz: 9ea2939b0f5fd94cad620376561def6cc0db832f0f62f990cdf66799c91c3bb6
5
5
  SHA512:
6
- metadata.gz: 3d496817c30ae59b7f99704fead73cc9dfe5d926a87455a5c51fcc766bdf299d943ce86c0d43ca859d870a488df191cc0a4bdeba302b95d8ebcf084730743a68
7
- data.tar.gz: 6291785257c054baf3570fb9baef4ecb1db006c920608ad69239460e2494610fa9133341bdf903a4c127f7ab40146f2bf9eecb34266621cba172a08ef52812f0
6
+ metadata.gz: acc1d2aa4fa8c76b2028cf2cc74b806b08ffc3e42a1e41d48326c53145fc6d3c95e6e724db9960e808b5c2f80330d4973caec143d97bf644f0f800348782476c
7
+ data.tar.gz: ddf8af09d31cb72577ceb3b4f2ef7fb90f0e813790966d532cc82690d9e9f56847cf8f391c36e860c6f51747dd795394f943941d6ecb44045e9ba424819b5854
@@ -53,7 +53,9 @@ module JsonRpcHandler
53
53
  end
54
54
  end
55
55
 
56
- def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
56
+ # `on_generate_error`, when given, is called with the `JSON::GeneratorError` of a response that cannot be generated,
57
+ # before that response is replaced with Internal error.
58
+ def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, on_generate_error: nil, &method_finder)
57
59
  begin
58
60
  begin
59
61
  request = JSON.parse(request_json, symbolize_names: true)
@@ -77,7 +79,35 @@ module JsonRpcHandler
77
79
  })
78
80
  end
79
81
 
80
- response&.to_json
82
+ begin
83
+ response&.to_json
84
+ rescue JSON::GeneratorError => e
85
+ on_generate_error&.call(e)
86
+ encodable_response(response, id_validation_pattern: id_validation_pattern).to_json
87
+ end
88
+ end
89
+
90
+ # `response` with each part JSON cannot generate replaced with Internal error. A batch is replaced item by item,
91
+ # so the other items keep their results.
92
+ def encodable_response(response, id_validation_pattern: nil)
93
+ return response.map { |item| encodable_response(item, id_validation_pattern: id_validation_pattern) } if response.is_a?(Array)
94
+
95
+ response.to_json
96
+ response
97
+ rescue JSON::GeneratorError
98
+ internal_error_for(response, id_validation_pattern: id_validation_pattern)
99
+ end
100
+
101
+ # The error answering a request whose response cannot be generated: a result or an error holding a string that
102
+ # is not valid UTF-8, typically one the request carried and the SDK or a handler repeated, such as a tool name
103
+ # in its Tool not found error. It carries the request id when the response has one, which was validated before
104
+ # the method ran.
105
+ def internal_error_for(response, id_validation_pattern: nil)
106
+ id = response[:id] if response.is_a?(Hash)
107
+ error_response(id: id.nil? ? :unknown_id : id, id_validation_pattern: id_validation_pattern, error: {
108
+ code: ErrorCode::INTERNAL_ERROR,
109
+ message: "Internal error",
110
+ })
81
111
  end
82
112
 
83
113
  def process_request(request, id_validation_pattern:, &method_finder)
@@ -434,7 +434,7 @@ module MCP
434
434
  line = read_line(method, params)
435
435
  raise_connection_error!(method, params) if line.nil?
436
436
 
437
- parsed = JSON.parse(line.strip)
437
+ parsed = JSON.parse(strip_frame(line))
438
438
 
439
439
  # A frame carrying `method` is a request or notification, never the awaited response,
440
440
  # whatever its id says: JSON-RPC id spaces are per sender, so a server-chosen ping id
@@ -484,8 +484,10 @@ module MCP
484
484
  parsed["method"] == MCP::Methods::PING && json_rpc_id?(parsed["id"]) && JsonRpcHandler.valid_version?(parsed["jsonrpc"])
485
485
  end
486
486
 
487
+ # An id the pong could not write back is skipped too: a String that is not valid UTF-8, or a number
488
+ # such as `1e400`, which the parser reads as an infinite Float that JSON cannot represent.
487
489
  def json_rpc_id?(id)
488
- id.is_a?(String) || id.is_a?(Numeric)
490
+ (id.is_a?(String) && id.valid_encoding?) || id.is_a?(Integer) || (id.is_a?(Float) && id.finite?)
489
491
  end
490
492
 
491
493
  def ensure_running!
@@ -534,6 +536,14 @@ module MCP
534
536
  )
535
537
  end
536
538
 
539
+ # Strips a frame as bytes. The server's stdout is read as UTF-8, and `String#strip` raises for a UTF-8 string
540
+ # holding an invalid byte at either end on every supported Ruby, and anywhere in it from Ruby 3.2 on, which would
541
+ # surface as that exception instead of the client's own error. As bytes, the frame reaches the parser, which
542
+ # refuses what is not JSON with the client's parse failure.
543
+ def strip_frame(line)
544
+ line.b.strip.force_encoding(Encoding::UTF_8)
545
+ end
546
+
537
547
  def raise_connection_error!(method, params)
538
548
  raise RequestHandlerError.new(
539
549
  "Server process closed stdout unexpectedly",
@@ -521,12 +521,19 @@ module MCP
521
521
  # a per-request sink for its ephemeral session and flushes it as SSE frames ahead of the final response.
522
522
  # Checked before the stateless guard, since modern requests are served in stateless deployments too.
523
523
  # The sink is bounded; past `MAX_MODERN_REQUEST_NOTIFICATIONS` the notification is dropped as
524
- # non-delivery (`false`), never handed to the legacy paths below.
524
+ # non-delivery (`false`), never handed to the legacy paths below. It holds each notification as JSON,
525
+ # generated here so that one that cannot be generated is dropped on its own instead of failing
526
+ # the whole response when the sink is flushed.
525
527
  sink = @mutex.synchronize { session_id && @modern_request_sinks[session_id] }
526
528
  if sink
527
529
  return false if sink.size >= MAX_MODERN_REQUEST_NOTIFICATIONS
528
530
 
529
- sink << notification
531
+ begin
532
+ sink << notification.to_json
533
+ rescue JSON::GeneratorError => e
534
+ MCP.configuration.exception_reporter.call(e, { session_id: session_id, error: "Failed to generate notification" })
535
+ return false
536
+ end
530
537
  return true
531
538
  end
532
539
 
@@ -931,16 +938,26 @@ module MCP
931
938
  # `nil` covers notifications and cancellation-suppressed responses; ack with 202 like the legacy notification path.
932
939
  return handle_accepted if response.nil?
933
940
 
941
+ # A response that cannot be generated answers its request with Internal error, as `JsonRpcHandler.handle_json`
942
+ # does on the legacy paths, instead of failing the whole exchange with a 500.
943
+ response_json = begin
944
+ response.to_json
945
+ rescue JSON::GeneratorError => e
946
+ MCP.configuration.exception_reporter.call(e, { error: "Failed to generate response" })
947
+ response = JsonRpcHandler.encodable_response(response)
948
+ response.to_json
949
+ end
950
+
934
951
  # Notifications a handler emitted during the request ride the request's own response stream as SSE frames ahead of
935
952
  # the final response (SEP-2575); a request that emitted none keeps the single JSON exchange and its HTTP status ladder.
936
953
  # Delivery is buffered: frames flush after the handler returns, preserving order but not real-time interleaving
937
954
  # (the TypeScript and Python SDKs stream live), and the sink grows with the handler's notification count.
938
955
  # Live streaming can follow without changing the wire shape.
939
956
  if notifications.empty?
940
- [modern_http_status(response), { "content-type" => "application/json" }, [response.to_json]]
957
+ [modern_http_status(response), { "content-type" => "application/json" }, [response_json]]
941
958
  else
942
- frames = notifications + [response]
943
- sse_body = frames.map { |frame| "event: message\ndata: #{frame.to_json}\n\n" }.join
959
+ frames = notifications + [response_json]
960
+ sse_body = frames.map { |frame| "event: message\ndata: #{frame}\n\n" }.join
944
961
  [200, SSE_HEADERS.dup, [sse_body]]
945
962
  end
946
963
  rescue StandardError => e
@@ -1034,7 +1051,7 @@ module MCP
1034
1051
 
1035
1052
  begin
1036
1053
  if RequestEnvelope.modern?(params)
1037
- RequestEnvelope.parse!(params, request: params)
1054
+ envelope = RequestEnvelope.parse!(params, request: params)
1038
1055
  else
1039
1056
  return invalid_request_response("Invalid Request: modern requests require the SEP-2575 `_meta` envelope")
1040
1057
  end
@@ -1074,7 +1091,7 @@ module MCP
1074
1091
  return too_many_listen_subscriptions_response(request_id)
1075
1092
  end
1076
1093
 
1077
- honored = honored_filter(filter)
1094
+ honored = honored_filter(filter, meta: params[:_meta], envelope: envelope, auth_info: auth_info)
1078
1095
 
1079
1096
  # The acknowledgement echoes the honored filter, whose resource URIs are client-supplied strings,
1080
1097
  # so it is encoded before the stream is committed to: a URI that cannot be written as JSON is
@@ -1321,8 +1338,9 @@ module MCP
1321
1338
 
1322
1339
  # Per SEP-2575, the server MUST NOT send notification types the client has not requested,
1323
1340
  # and the acknowledgement only includes types the server actually supports
1324
- # (derived from its declared capabilities).
1325
- def honored_filter(filter)
1341
+ # (derived from its declared capabilities). Resource URIs are further narrowed to those
1342
+ # the server's `resources_subscribe_handler` accepts; the field is omitted when none remain.
1343
+ def honored_filter(filter, meta: nil, envelope: nil, auth_info: nil)
1326
1344
  capabilities = @server.capabilities
1327
1345
  honored = {}
1328
1346
  honored[:toolsListChanged] = true if filter[:toolsListChanged] && capability_flag?(capabilities, :tools, :listChanged)
@@ -1331,7 +1349,8 @@ module MCP
1331
1349
 
1332
1350
  subscriptions = filter[:resourceSubscriptions]
1333
1351
  if capability_flag?(capabilities, :resources, :subscribe) && subscriptions.is_a?(Array) && !subscriptions.empty?
1334
- honored[:resourceSubscriptions] = subscriptions.uniq
1352
+ uris = @server.subscribable_resource_uris(subscriptions, meta: meta, envelope: envelope, auth_info: auth_info)
1353
+ honored[:resourceSubscriptions] = uris unless uris.empty?
1335
1354
  end
1336
1355
 
1337
1356
  honored
data/lib/mcp/server.rb CHANGED
@@ -303,7 +303,8 @@ module MCP
303
303
  # for the current request, as in `handle`.
304
304
  # @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
305
305
  def handle_json(request, session: nil, auth_info: nil)
306
- JsonRpcHandler.handle_json(request, id_validation_pattern: nil) do |method, request_id|
306
+ on_generate_error = ->(error) { report_exception(error, { error: "Failed to generate response" }) }
307
+ JsonRpcHandler.handle_json(request, id_validation_pattern: nil, on_generate_error: on_generate_error) do |method, request_id|
307
308
  handle_request(request, method, session: session, related_request_id: request_id, auth_info: auth_info)
308
309
  end
309
310
  end
@@ -500,6 +501,36 @@ module MCP
500
501
  @handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
501
502
  end
502
503
 
504
+ # Returns the distinct members of `uris` that the `resources_subscribe_handler` accepts, calling it once per URI
505
+ # with `{ uri: }`, plus `_meta` when given. `subscriptions/listen` takes the place of `resources/subscribe`
506
+ # in the 2026-07-28 lifecycle, so the handler keeps deciding which resources a client may follow there too.
507
+ # A URI the handler raises for is reported and left out, as the spec lets the acknowledgement honor a subset of
508
+ # what was requested; any return value accepts the URI and is otherwise discarded. The `server_context` has no session,
509
+ # related request id, or cancellation. Public only so that `StreamableHTTPTransport` can call it.
510
+ #
511
+ # @api private
512
+ # @param uris [Array<String>] The `resourceSubscriptions` of a `subscriptions/listen` request.
513
+ # @param meta [Hash, nil] The `_meta` of that request.
514
+ # @param envelope [MCP::RequestEnvelope, nil] The envelope of that request.
515
+ # @param auth_info [MCP::Server::OAuth::AccessToken, nil] The token that authenticated that request.
516
+ # @return [Array<String>]
517
+ def subscribable_resource_uris(uris, meta: nil, envelope: nil, auth_info: nil)
518
+ handler = @handlers[Methods::RESOURCES_SUBSCRIBE]
519
+
520
+ uris.uniq.select do |uri|
521
+ params = { uri: uri }
522
+ params[:_meta] = meta if meta
523
+ dispatch_optional_context_handler(handler, params, envelope: envelope, auth_info: auth_info)
524
+ true
525
+ rescue RequestHandlerError => e
526
+ report_exception(e.original_error || e, { request: params })
527
+ false
528
+ rescue => e
529
+ report_exception(e, { request: params })
530
+ false
531
+ end
532
+ end
533
+
503
534
  def build_sampling_params(
504
535
  capabilities,
505
536
  messages:,
@@ -275,14 +275,14 @@ module MCP
275
275
  def notify_elicitation_complete(elicitation_id:)
276
276
  send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id })
277
277
  rescue => e
278
- @server.report_exception(e, notification: "elicitation_complete")
278
+ @server.configuration.exception_reporter.call(e, { notification: "elicitation_complete" })
279
279
  end
280
280
 
281
281
  # Sends a resource updated notification to this session only.
282
282
  def notify_resources_updated(uri:)
283
283
  send_to_transport(Methods::NOTIFICATIONS_RESOURCES_UPDATED, { "uri" => uri })
284
284
  rescue => e
285
- @server.report_exception(e, notification: "resources_updated")
285
+ @server.configuration.exception_reporter.call(e, { notification: "resources_updated" })
286
286
  end
287
287
 
288
288
  # Sends a progress notification to this session only.
@@ -296,7 +296,7 @@ module MCP
296
296
 
297
297
  send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params, related_request_id: related_request_id)
298
298
  rescue => e
299
- @server.report_exception(e, notification: "progress")
299
+ @server.configuration.exception_reporter.call(e, { notification: "progress" })
300
300
  end
301
301
 
302
302
  # Sends a log message notification to this session only.
@@ -320,7 +320,7 @@ module MCP
320
320
 
321
321
  send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params, related_request_id: related_request_id)
322
322
  rescue => e
323
- @server.report_exception(e, { notification: "log_message" })
323
+ @server.configuration.exception_reporter.call(e, { notification: "log_message" })
324
324
  end
325
325
 
326
326
  private
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "1.8.1"
4
+ VERSION = "1.8.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.1
4
+ version: 1.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -119,7 +119,7 @@ licenses:
119
119
  - Apache-2.0
120
120
  metadata:
121
121
  allowed_push_host: https://rubygems.org
122
- changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.8.1
122
+ changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.8.2
123
123
  homepage_uri: https://ruby.sdk.modelcontextprotocol.io
124
124
  source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
125
125
  bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues