mcp 1.6.0 → 1.6.1

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: 3cb0f742e469efeb15202b40310bfb2eab52b94bb208bab1f33cfd2204bcf8c9
4
- data.tar.gz: 07df380a1dd652b6fd5deb0ddbafe3c3379bc6fa77c8a391f169f6a7fed11742
3
+ metadata.gz: '08c2de444c55244db435a10ce7b9f9026d24158d47cadd6f89b917f42e09b534'
4
+ data.tar.gz: 6126a02c9adf92df07f22b323a0337f2abae9eb23c7bf014f1b7ef1ccbbf3a72
5
5
  SHA512:
6
- metadata.gz: c756d7552fbf3accc8eedc32876e715c1d34939ed8c44d9d540a97b46ccf4cc2d3a163f5ba5989277be6e8933bce374ccab80f27ddf62ed52f6ea27c0367a3c5
7
- data.tar.gz: '0873ea9933d9b30e1c0e2bcbd6f56eb0c658f16736ae309007cd752b097cb9f75ebcead7277c89ea9f255f92f3f62d2d4f1ac5d4ae87cf36359df4e42e030d38'
6
+ metadata.gz: 7318acb46d46e9c958115f95171f21b20c186ab32608770d443440e0202d29319b9384b11099ed69f136c3ba9146be39998e1a9a47a839fe506e07cddca0ad89
7
+ data.tar.gz: 3008f7f6061e0a2cd514d377ade38a03be1ed88d863fb268e50c9820ece43f585b7fcc801bc6c0ffcf78fab3a80af3397c20e497722a6821c55d6128d391d4fc
data/README.md CHANGED
@@ -125,7 +125,7 @@ see [Client Transports](https://ruby.sdk.modelcontextprotocol.io/client/transpor
125
125
 
126
126
  ## Examples
127
127
 
128
- Runnable examples are available in [`examples/`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples),
128
+ Runnable examples are available in [`examples`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples),
129
129
  including a complete Rails application in [`examples/rails`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples/rails).
130
130
 
131
131
  ## Documentation
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ipaddr"
4
+ require "strscan"
4
5
  require "uri"
5
6
 
6
7
  module MCP
@@ -43,6 +44,12 @@ module MCP
43
44
  # or a bare token, per RFC 7235.
44
45
  WWW_AUTH_PARAM_PATTERN = /\A([A-Za-z0-9_-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/.freeze
45
46
 
47
+ # The whitespace and optional comma between two `key=value` pairs, or before the first one.
48
+ WWW_AUTH_PARAM_SEPARATOR_PATTERN = /\s*,?\s*/.freeze
49
+
50
+ # The `Bearer` challenge: at the start of the header or after a comma.
51
+ WWW_AUTH_BEARER_PATTERN = /(?:\A|,)\s*Bearer(?:\s+|\z)/i.freeze
52
+
46
53
  class << self
47
54
  # Parses a `WWW-Authenticate` header and returns the parameters of
48
55
  # the `Bearer` challenge as a hash with lower-cased keys (e.g. `resource_metadata`,
@@ -55,25 +62,21 @@ module MCP
55
62
  def parse_www_authenticate(header)
56
63
  return {} unless header
57
64
 
58
- # Locate the Bearer challenge: at the start of the header or after a comma.
59
- bearer = header.match(/(?:\A|,)\s*Bearer(?:\s+|\z)/i)
60
- return {} unless bearer
61
-
62
65
  # Walk key=value pairs starting where Bearer's parameters begin.
63
- # The loop stops at the first token that is not a key=value pair,
64
- # which marks the next challenge (e.g. `, DPoP algs="..."`).
65
- cursor = bearer.end(0)
66
- params = {}
67
- while cursor < header.length
68
- prefix = header[cursor..]
69
- prefix = prefix.sub(/\A\s*,?\s*/, "")
70
- break if prefix.empty?
66
+ # The loop stops at the first token that is not a key=value pair, which marks the next challenge (e.g. `, DPoP algs="..."`).
67
+ # The scanner keeps the walk linear in the header's length: slicing off the consumed prefix instead copies the remainder
68
+ # for every pair, and the server chooses how many pairs it sends. The header is also the server's to fill: a byte sequence
69
+ # that is not valid in the string's encoding would make the patterns raise `ArgumentError`, so such bytes are replaced first.
70
+ scanner = StringScanner.new(header.scrub)
71
+ return {} unless scanner.skip_until(WWW_AUTH_BEARER_PATTERN)
71
72
 
72
- match = prefix.match(WWW_AUTH_PARAM_PATTERN)
73
- break unless match
73
+ params = {}
74
+ until scanner.eos?
75
+ scanner.skip(WWW_AUTH_PARAM_SEPARATOR_PATTERN)
76
+ break if scanner.eos?
77
+ break unless scanner.scan(WWW_AUTH_PARAM_PATTERN)
74
78
 
75
- params[match[1].downcase] = match[2] ? unescape_quoted_pair(match[2]) : match[3]
76
- cursor = header.length - prefix.length + match.end(0)
79
+ params[scanner[1].downcase] = scanner[2] ? unescape_quoted_pair(scanner[2]) : scanner[3]
77
80
  end
78
81
  params
79
82
  end
@@ -576,47 +579,46 @@ module MCP
576
579
  end.join("&")
577
580
  end
578
581
 
579
- # Implements RFC 3986 Section 5.2.4 `remove_dot_segments`. Walks the input
580
- # buffer one segment at a time, popping the previous output segment
581
- # whenever a `..` is encountered, so that `/api/../mcp` collapses to
582
- # `/mcp` and `/foo/./bar` collapses to `/foo/bar`.
582
+ # Implements RFC 3986 Section 5.2.4 `remove_dot_segments` over the path's segments, so that `/api/../mcp` collapses to
583
+ # `/mcp` and `/foo/./bar` collapses to `/foo/bar`. Each segment is visited once: the RFC's buffer rewriting,
584
+ # applied literally, copies the remaining input for every dot segment, and the path is the server's to choose.
585
+ #
586
+ # The output matches the RFC's algorithm for a relative path as well, including its quirk that a `..` popping
587
+ # the first segment leaves the result absolute (`a/../b` becomes `/b`), although `URI#path` never hands over a relative path.
588
+ # The rule letters below are the RFC's own: steps A through E of its loop.
583
589
  # https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4
584
590
  def remove_dot_segments(path)
585
591
  return path if path.nil? || path.empty?
586
592
 
587
- input = path.dup
588
- output = +""
589
- until input.empty?
590
- if input.start_with?("../")
591
- input = input[3..]
592
- elsif input.start_with?("./")
593
- input = input[2..]
594
- elsif input.start_with?("/./")
595
- input = "/#{input[3..]}"
596
- elsif input == "/."
597
- input = "/"
598
- elsif input.start_with?("/../")
599
- input = "/#{input[4..]}"
600
- output = remove_last_segment(output)
601
- elsif input == "/.."
602
- input = "/"
603
- output = remove_last_segment(output)
604
- elsif input == "." || input == ".."
605
- input = ""
593
+ absolute = path.start_with?("/")
594
+ segments = path.split("/", -1)
595
+ segments.shift if absolute
596
+
597
+ output = []
598
+
599
+ # True until a segment other than `.` or `..` is kept: a relative path's leading `./` and `../` are dropped together with
600
+ # the slash after them (Rule A), so the segment that follows is still the slash-less first one.
601
+ leading = !absolute
602
+ segments.each_with_index do |segment, index|
603
+ last = index == segments.length - 1
604
+
605
+ # Rule A, and Rule D for a path that is nothing but `.` or `..`.
606
+ next if leading && (segment == "." || segment == "..")
607
+
608
+ if segment == "."
609
+ # Rule B: `/./` disappears; a final `/.` leaves its slash behind.
610
+ output << "/" if last
611
+ elsif segment == ".."
612
+ # Rule C: `/../` removes the previous segment; a final `/..` leaves its slash behind.
613
+ output.pop
614
+ output << "/" if last
606
615
  else
607
- segment = input.match(%r{\A/?[^/]*})[0]
608
- output << segment
609
- input = input[segment.length..]
616
+ # Rule E: the first segment of a relative path carries no slash.
617
+ output << (leading ? segment : "/#{segment}")
610
618
  end
619
+ leading = false
611
620
  end
612
- output
613
- end
614
-
615
- def remove_last_segment(output)
616
- idx = output.rindex("/")
617
- return +"" if idx.nil?
618
-
619
- output[0...idx]
621
+ output.join
620
622
  end
621
623
  end
622
624
  end
@@ -1206,16 +1206,30 @@ module MCP
1206
1206
  "Authorization server metadata `authorization_endpoint` is not a valid URI: #{e.message}."
1207
1207
  end
1208
1208
 
1209
- params = URI.decode_www_form(uri.query.to_s)
1210
- params << ["response_type", "code"]
1211
- params << ["client_id", client_id]
1212
- params << ["redirect_uri", @provider.redirect_uri]
1213
- params << ["code_challenge", code_challenge]
1214
- params << ["code_challenge_method", "S256"]
1215
- params << ["state", state]
1216
- params << ["scope", scope] if scope
1217
- params << ["resource", resource] if resource
1218
- uri.query = URI.encode_www_form(params)
1209
+ # A parameter the flow sets replaces any of the same name the endpoint URL already carries.
1210
+ # RFC 6749 Section 3.1 forbids sending a parameter twice, and which of two values a server would honor is
1211
+ # its own choice; on the legacy path the endpoint URL is served by the MCP server, whose query must not speak
1212
+ # for the client's `client_id`, `redirect_uri`, `code_challenge`, or `resource`.
1213
+ # Other parameters in the URL are kept, as the TypeScript SDK's `searchParams.set` keeps them; that includes
1214
+ # a `scope` when the flow has none, since an authorization server may set a default scope there.
1215
+ # RFC 9101 `request` and `request_uri` are dropped as well, though the flow sets neither: a server takes
1216
+ # the whole authorization request from the object they carry, over every parameter in the query, and both are
1217
+ # the client's to send, never an endpoint URL's to supply.
1218
+ own_params = [
1219
+ ["response_type", "code"],
1220
+ ["client_id", client_id],
1221
+ ["redirect_uri", @provider.redirect_uri],
1222
+ ["code_challenge", code_challenge],
1223
+ ["code_challenge_method", "S256"],
1224
+ ["state", state],
1225
+ ]
1226
+ own_params << ["scope", scope] if scope
1227
+ own_params << ["resource", resource] if resource
1228
+ dropped_names = own_params.map(&:first) + ["request", "request_uri"]
1229
+
1230
+ params = URI.decode_www_form(uri.query.to_s).reject { |name, _value| dropped_names.include?(name) }
1231
+ uri.query = URI.encode_www_form(params + own_params)
1232
+
1219
1233
  uri
1220
1234
  end
1221
1235
 
@@ -169,8 +169,12 @@ module MCP
169
169
  @allowed_origins = Array(allowed_origins).map(&:downcase).freeze
170
170
  @pending_responses = {}
171
171
 
172
- # Maps a `subscriptions/listen` request id to
173
- # `{ stream: stream_object, filter: honored_subscription_filter, active: boolean, write_mutex: Mutex }` (SEP-2575).
172
+ # Maps a key the transport mints for each `subscriptions/listen` stream to
173
+ # `{ request_id: listen_request_id, stream: stream_object, filter: honored_subscription_filter, active: boolean,
174
+ # write_mutex: Mutex, keepalive_wakeup: ConditionVariable }` (SEP-2575). The request id is the client's,
175
+ # unique only among that client's own in-flight requests, so it stamps `subscriptionId` but cannot serve as the key:
176
+ # two clients may pick the same one. Whoever removes an entry signals `keepalive_wakeup` under `@mutex`,
177
+ # so the stream's keepalive thread ends with its slot instead of sleeping out its interval.
174
178
  # In-process only; a multi-worker deployment needs an external event bus to fan notifications out across processes,
175
179
  # which is a follow-up.
176
180
  @listen_subscriptions = {}
@@ -925,23 +929,33 @@ module MCP
925
929
  # the legacy GET stream (`create_sse_body`).
926
930
  #
927
931
  # Registration and activation are split on purpose: the entry is inserted inactive
928
- # (reserving the id and the cap slot atomically), the acknowledgement is written outside the lock,
932
+ # (reserving the cap slot atomically), the acknowledgement is written outside the registry lock,
929
933
  # and only then does the entry become eligible for delivery. A concurrent notification between
930
934
  # the insert and the acknowledgement write skips the inactive entry,
931
935
  # enforcing the SEP-2575 rule that no notification precedes the acknowledgement.
936
+ #
937
+ # The acknowledgement write holds the stream's write mutex, which `teardown_listen_subscriptions` also takes
938
+ # before it marks an entry closed. The two therefore cannot interleave: the acknowledgement either lands
939
+ # before the result, or, if the transport closed first, is not written at all and the stream just closes,
940
+ # so no stream ever carries a result ahead of its acknowledgement.
941
+ #
942
+ # The entry is keyed by an identifier minted here, not by the request id: that id is unique only among
943
+ # the requesting client's own in-flight requests, and two clients that pick the same one must each get
944
+ # their stream, stamped with the id they sent.
932
945
  def listen_sse_body(request_id, honored)
933
946
  ListenStreamBody.new do |stream|
934
- rejected = false
947
+ subscription_key = SecureRandom.uuid
948
+ subscription = nil
935
949
  @mutex.synchronize do
936
- if @listen_subscriptions.key?(request_id) ||
937
- (@max_listen_subscriptions && @listen_subscriptions.size >= @max_listen_subscriptions)
938
- rejected = true
939
- else
940
- @listen_subscriptions[request_id] = { stream: stream, filter: honored, active: false, write_mutex: Mutex.new }
950
+ unless @max_listen_subscriptions && @listen_subscriptions.size >= @max_listen_subscriptions
951
+ subscription = {
952
+ request_id: request_id, stream: stream, filter: honored, active: false, write_mutex: Mutex.new, keepalive_wakeup: ConditionVariable.new
953
+ }
954
+ @listen_subscriptions[subscription_key] = subscription
941
955
  end
942
956
  end
943
957
 
944
- if rejected
958
+ if subscription.nil?
945
959
  close_stream_safely(stream)
946
960
  else
947
961
  acknowledgement = {
@@ -954,66 +968,92 @@ module MCP
954
968
  }
955
969
 
956
970
  begin
957
- send_to_stream(stream, acknowledgement)
958
- activate_listen_subscription(request_id)
959
- start_listen_keepalive_thread(request_id)
971
+ acknowledged = subscription[:write_mutex].synchronize do
972
+ next false if subscription[:closed]
973
+
974
+ send_to_stream(stream, acknowledgement)
975
+
976
+ # Set on the entry itself, not through the registry: a concurrent close may already have cleared the registry
977
+ # while its result write waits on this mutex, and that write must still find the stream acknowledged.
978
+ # Set under the registry lock as well, since that is the lock the delivery snapshot reads the flag under.
979
+ # This is the one place a write mutex is held while the registry lock is taken; it stays deadlock-free only
980
+ # as long as no path takes a write mutex inside `@mutex.synchronize`, so resolve entries under `@mutex`,
981
+ # release it, then write.
982
+ @mutex.synchronize { subscription[:active] = true }
983
+
984
+ true
985
+ end
986
+
987
+ if acknowledged
988
+ start_listen_keepalive_thread(subscription_key, request_id)
989
+ else
990
+ close_stream_safely(stream)
991
+ end
960
992
  rescue *STREAM_WRITE_ERRORS
961
- remove_listen_subscription(request_id)
993
+ remove_listen_subscription(subscription_key)
962
994
  close_stream_safely(stream)
963
995
  end
964
996
  end
965
997
  end
966
998
  end
967
999
 
968
- # Marks a listen subscription eligible for delivery once its acknowledgement write has completed.
969
- # The entry may already be gone when the transport closed concurrently.
970
- def activate_listen_subscription(request_id)
971
- @mutex.synchronize do
972
- subscription = @listen_subscriptions[request_id]
973
- subscription[:active] = true if subscription
974
- end
975
- end
976
-
977
1000
  # Periodically writes an SSE keepalive comment frame to a listen stream so a silently dropped
978
1001
  # connection is detected and its slot freed, rather than held until the next fan-out write.
979
1002
  # Mirrors the legacy GET stream's `start_keepalive_thread`; a comment frame (not a data frame)
980
1003
  # cannot corrupt an interleaved notification's JSON.
981
- def start_listen_keepalive_thread(request_id)
1004
+ #
1005
+ # The wait between pings is a condition variable wait under `@mutex`, not a plain sleep:
1006
+ # the presence check and the wait happen under the same lock that removals signal from,
1007
+ # so a removal cannot slip in between them and a thread waiting out its interval wakes
1008
+ # at once when its entry goes, whichever path removed it. A thread already past the wait,
1009
+ # in a ping, finishes that write first and then finds its entry gone.
1010
+ def start_listen_keepalive_thread(subscription_key, request_id)
982
1011
  return unless @listen_keepalive_interval
983
1012
 
984
1013
  Thread.new do
985
- while listen_subscription_active?(request_id)
986
- sleep(@listen_keepalive_interval)
987
- send_listen_keepalive_ping(request_id)
1014
+ loop do
1015
+ registered = @mutex.synchronize do
1016
+ subscription = @listen_subscriptions[subscription_key]
1017
+ next false unless subscription
1018
+
1019
+ subscription[:keepalive_wakeup].wait(@mutex, @listen_keepalive_interval)
1020
+
1021
+ @listen_subscriptions.key?(subscription_key)
1022
+ end
1023
+ break unless registered
1024
+
1025
+ send_listen_keepalive_ping(subscription_key)
988
1026
  end
989
1027
  rescue *STREAM_WRITE_ERRORS
990
1028
  # The peer went away; the ensure frees the slot. A dropped listen stream is the normal
991
1029
  # way this loop ends, so it is not reported.
992
1030
  rescue StandardError => e
1031
+ # The request id is taken from the caller rather than the registry: a delivery failure may have
1032
+ # removed the entry already, and the report should still name the stream.
993
1033
  MCP.configuration.exception_reporter.call(e, { subscription_id: request_id })
994
1034
  ensure
995
1035
  stream = @mutex.synchronize do
996
- subscription = @listen_subscriptions.delete(request_id)
1036
+ subscription = @listen_subscriptions.delete(subscription_key)
997
1037
  subscription && subscription[:stream]
998
1038
  end
999
1039
  close_stream_safely(stream) if stream
1000
1040
  end
1001
1041
  end
1002
1042
 
1003
- def listen_subscription_active?(request_id)
1004
- @mutex.synchronize { @listen_subscriptions.key?(request_id) }
1005
- end
1043
+ # Resolves the entry under the registry lock, then writes outside it so a stalled reader cannot
1044
+ # block every other subscription on `@mutex`. The write itself holds the stream's write mutex,
1045
+ # like notification delivery and the closing result: the comment frame then cannot land between
1046
+ # the bytes of a notification or after the closing result, and once teardown has marked
1047
+ # the entry closed the ping is skipped. A write error propagates to end the keepalive loop.
1048
+ def send_listen_keepalive_ping(subscription_key)
1049
+ subscription = @mutex.synchronize { @listen_subscriptions[subscription_key] }
1050
+ return unless subscription
1006
1051
 
1007
- # Resolves the stream under the lock, then writes outside it so a stalled reader cannot block
1008
- # every other subscription on `@mutex`. A write error propagates to end the keepalive loop.
1009
- def send_listen_keepalive_ping(request_id)
1010
- stream = @mutex.synchronize do
1011
- subscription = @listen_subscriptions[request_id]
1012
- subscription && subscription[:stream]
1013
- end
1014
- return unless stream
1052
+ subscription[:write_mutex].synchronize do
1053
+ next if subscription[:closed]
1015
1054
 
1016
- send_ping_to_stream(stream)
1055
+ send_ping_to_stream(subscription[:stream])
1056
+ end
1017
1057
  end
1018
1058
 
1019
1059
  # Per SEP-2575, the server MUST NOT send notification types the client has not requested,
@@ -1054,7 +1094,7 @@ module MCP
1054
1094
  # The matching snapshot is taken under `@mutex`, but stream writes happen outside it:
1055
1095
  # a slow or stalled subscriber must not block the transport, matching the legacy delivery paths.
1056
1096
  matched = @mutex.synchronize do
1057
- @listen_subscriptions.filter_map do |request_id, subscription|
1097
+ @listen_subscriptions.filter_map do |subscription_key, subscription|
1058
1098
  # An inactive entry has not finished writing its acknowledgement yet;
1059
1099
  # delivering to it would put a notification ahead of the acknowledgement.
1060
1100
  next unless subscription[:active]
@@ -1067,12 +1107,12 @@ module MCP
1067
1107
  uris.is_a?(Array) && uris.include?(uri)
1068
1108
  end
1069
1109
 
1070
- [request_id, subscription] if hit
1110
+ [subscription_key, subscription] if hit
1071
1111
  end
1072
1112
  end
1073
1113
 
1074
- matched.each do |request_id, subscription|
1075
- meta = { RequestEnvelope::SUBSCRIPTION_ID_META_KEY.to_sym => request_id }
1114
+ matched.each do |subscription_key, subscription|
1115
+ meta = { RequestEnvelope::SUBSCRIPTION_ID_META_KEY.to_sym => subscription[:request_id] }
1076
1116
  notification_params = (params || {}).merge(_meta: meta)
1077
1117
  notification = { jsonrpc: "2.0", method: method, params: notification_params }
1078
1118
 
@@ -1089,16 +1129,21 @@ module MCP
1089
1129
  rescue *STREAM_WRITE_ERRORS => e
1090
1130
  MCP.configuration.exception_reporter.call(
1091
1131
  e,
1092
- { subscription_id: request_id, error: "Failed to send notification" },
1132
+ { subscription_id: subscription[:request_id], error: "Failed to send notification" },
1093
1133
  )
1094
- remove_listen_subscription(request_id)
1134
+ remove_listen_subscription(subscription_key)
1095
1135
  close_stream_safely(subscription[:stream])
1096
1136
  end
1097
1137
  end
1098
1138
  end
1099
1139
 
1100
- def remove_listen_subscription(request_id)
1101
- @mutex.synchronize { @listen_subscriptions.delete(request_id) }
1140
+ def remove_listen_subscription(subscription_key)
1141
+ @mutex.synchronize do
1142
+ subscription = @listen_subscriptions.delete(subscription_key)
1143
+ subscription[:keepalive_wakeup].signal if subscription
1144
+
1145
+ subscription
1146
+ end
1102
1147
  end
1103
1148
 
1104
1149
  # Graceful teardown (SEP-2575): each open listen stream receives its `SubscriptionsListenResult` response
@@ -1107,26 +1152,35 @@ module MCP
1107
1152
  removed = @mutex.synchronize do
1108
1153
  subscriptions = @listen_subscriptions.dup
1109
1154
  @listen_subscriptions.clear
1155
+
1156
+ subscriptions.each_value do |subscription|
1157
+ subscription[:keepalive_wakeup].signal
1158
+ end
1159
+
1110
1160
  subscriptions
1111
1161
  end
1112
1162
 
1113
- removed.each do |request_id, subscription|
1163
+ removed.each_value do |subscription|
1114
1164
  # Marking the entry closed and writing the result under the stream's write mutex orders
1115
- # this against in-flight deliveries: each one either lands before the result or observes
1116
- # `closed` and skips, keeping the graceful result the stream's final message.
1165
+ # this against in-flight deliveries and against the acknowledgement write: each one either lands
1166
+ # before the result or observes `closed` and skips, keeping the graceful result the stream's final message.
1117
1167
  subscription[:write_mutex].synchronize do
1118
1168
  subscription[:closed] = true
1119
1169
 
1170
+ # A stream whose acknowledgement was never written gets no result either: SEP-2575 makes
1171
+ # the acknowledgement the first message, so the stream closes abruptly and the client re-sends.
1172
+ next unless subscription[:active]
1173
+
1120
1174
  begin
1121
1175
  send_to_stream(subscription[:stream], {
1122
1176
  jsonrpc: "2.0",
1123
- id: request_id,
1177
+ id: subscription[:request_id],
1124
1178
  result: {
1125
1179
  # `SubscriptionsListenResult` is served at the transport layer and never
1126
1180
  # passes through the dispatch path, so the REQUIRED 2026-07-28 `resultType` is
1127
1181
  # stamped at its construction site.
1128
1182
  resultType: ResultType::COMPLETE,
1129
- _meta: { RequestEnvelope::SUBSCRIPTION_ID_META_KEY.to_sym => request_id },
1183
+ _meta: { RequestEnvelope::SUBSCRIPTION_ID_META_KEY.to_sym => subscription[:request_id] },
1130
1184
  },
1131
1185
  })
1132
1186
  rescue *STREAM_WRITE_ERRORS
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.6.0"
4
+ VERSION = "1.6.1"
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.6.0
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -108,7 +108,7 @@ licenses:
108
108
  - Apache-2.0
109
109
  metadata:
110
110
  allowed_push_host: https://rubygems.org
111
- changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.6.0
111
+ changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.6.1
112
112
  homepage_uri: https://ruby.sdk.modelcontextprotocol.io
113
113
  source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
114
114
  bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues