mcp 1.5.1 → 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.
@@ -95,7 +95,7 @@ module MCP
95
95
  assertion
96
96
  end
97
97
 
98
- # `Accept-Encoding` is deliberately left unset, for the same reason as `Flow#default_http_client`:
98
+ # `Accept-Encoding` is deliberately left unset, for the same reason as `Flow.build_http_client`:
99
99
  # claiming that header turns Net::HTTP's `decode_content` off and would move `BoundedBody`'s cap
100
100
  # onto compressed bytes.
101
101
  def default_http_client
@@ -48,6 +48,13 @@ module MCP
48
48
  # The document served at the URL is a separate JSON artifact from the `client_metadata` keyword:
49
49
  # DCR `client_metadata` MUST NOT include `client_id`, while the CIMD document MUST include `client_id` set
50
50
  # to the URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
51
+ # - `token_request_params` - Hash of String keys and values added to every token request this provider makes
52
+ # (the authorization code exchange and refresh), for parameters the authorization server requires beyond
53
+ # the grant itself. The authorization request is not affected. A key in `Flow::RESERVED_TOKEN_REQUEST_PARAMS`
54
+ # raises `Flow::InvalidTokenRequestParamsError`.
55
+ # The Hash is copied and frozen. See `StorageBackedProvider#token_request_params`.
56
+ # - `http_client_customizer` - Callable invoked with the `Faraday::Connection` the flow builds for
57
+ # its own requests; see `StorageBackedProvider#http_client_customizer`.
51
58
  class Provider
52
59
  include StorageBackedProvider
53
60
 
@@ -87,7 +94,9 @@ module MCP
87
94
  scope: nil,
88
95
  storage: nil,
89
96
  client_id_metadata_document_url: nil,
90
- authorization_request_validator: nil
97
+ authorization_request_validator: nil,
98
+ token_request_params: nil,
99
+ http_client_customizer: nil
91
100
  )
92
101
  unless Discovery.secure_url?(redirect_uri)
93
102
  raise InsecureRedirectURIError,
@@ -109,6 +118,8 @@ module MCP
109
118
  "per the MCP authorization specification and `draft-ietf-oauth-client-id-metadata-document`."
110
119
  end
111
120
 
121
+ http_client_customizer = validated_http_client_customizer(http_client_customizer)
122
+
112
123
  @client_metadata = client_metadata
113
124
  @redirect_uri = redirect_uri
114
125
  @redirect_handler = redirect_handler
@@ -117,6 +128,8 @@ module MCP
117
128
  @storage = storage || InMemoryStorage.new
118
129
  @client_id_metadata_document_url = client_id_metadata_document_url
119
130
  @authorization_request_validator = authorization_request_validator
131
+ @token_request_params = frozen_token_request_params(token_request_params)
132
+ @http_client_customizer = http_client_customizer
120
133
  end
121
134
 
122
135
  # Identifies the OAuth flow this provider drives.
@@ -4,11 +4,11 @@ module MCP
4
4
  class Client
5
5
  module OAuth
6
6
  # Shared token/credential persistence for the OAuth provider classes
7
- # (`Provider` for the authorization-code flow and `ClientCredentialsProvider`
8
- # for the client_credentials flow). The two grants differ in how they authenticate,
9
- # but both read and write the same two pieces of state through a `storage` object:
10
- # the token response and the client information. This module supplies that delegation
11
- # so the `Flow` orchestrator can treat any provider uniformly.
7
+ # (`Provider` for the authorization-code flow, `ClientCredentialsProvider`
8
+ # for the client_credentials flow, and `CrossAppAccessProvider` for the jwt-bearer flow).
9
+ # The grants differ in how they authenticate, but all read and write the same two pieces of state
10
+ # through a `storage` object: the token response and the client information. This module supplies
11
+ # that delegation so the `Flow` orchestrator can treat any provider uniformly.
12
12
  #
13
13
  # Including classes must set `@storage` to an object responding to `tokens`,
14
14
  # `save_tokens(tokens)`, `client_information`, and `save_client_information(info)`
@@ -21,6 +21,20 @@ module MCP
21
21
  # MCP SDK has today.
22
22
  attr_reader :authorization_request_validator
23
23
 
24
+ # Optional Hash of String keys and values added to every token request the provider makes, for parameters
25
+ # the authorization server requires beyond the grant itself (RFC 6749 Section 8.2 leaves room for them;
26
+ # Auth0's `audience` is the usual one). `nil` (the default) adds nothing.
27
+ # Set through `frozen_token_request_params`, which refuses a key `Flow` sets itself or a wrongly shaped value
28
+ # with `Flow::InvalidTokenRequestParamsError`.
29
+ attr_reader :token_request_params
30
+
31
+ # Optional callable invoked with the `Faraday::Connection` the flow builds for its own requests,
32
+ # after the SDK's defaults and before its origin guard (see `Flow.build_http_client`), so an application can
33
+ # add middleware to discovery, registration, and token requests or swap their adapter. `nil` (the default)
34
+ # keeps the default connection. The transport's own connection and customizer block never serve these requests:
35
+ # they are bound to the MCP server URL and carry headers meant for that server.
36
+ attr_reader :http_client_customizer
37
+
24
38
  def access_token
25
39
  tokens&.dig("access_token") || tokens&.dig(:access_token)
26
40
  end
@@ -44,6 +58,29 @@ module MCP
44
58
  def clear_tokens!
45
59
  @storage.save_tokens(nil)
46
60
  end
61
+
62
+ private
63
+
64
+ # Constructors call this before writing to `storage`, so a rejected provider leaves it untouched.
65
+ # The copy has its own frozen keys and values, so a later change to the caller's Hash, to a key,
66
+ # or to a value cannot alter what is sent to the token endpoint. Keys are copied explicitly
67
+ # because `Hash` copies and freezes only keys whose class is exactly `String`.
68
+ def frozen_token_request_params(params)
69
+ return if params.nil?
70
+
71
+ problem = Flow.token_request_params_problem(params)
72
+ raise Flow::InvalidTokenRequestParamsError, "token_request_params #{problem}" if problem
73
+
74
+ params.each_with_object({}) { |(key, value), copy| copy[key.dup.freeze] = value.dup.freeze }.freeze
75
+ end
76
+
77
+ # Returns `customizer` when it is `nil` or callable and raises otherwise, so a connection object passed
78
+ # in place of a callable fails at construction rather than at the first `401`.
79
+ def validated_http_client_customizer(customizer)
80
+ return customizer if customizer.nil? || customizer.respond_to?(:call)
81
+
82
+ raise ArgumentError, "http_client_customizer must respond to call (got #{customizer.class})."
83
+ end
47
84
  end
48
85
  end
49
86
  end
data/lib/mcp/icon.rb CHANGED
@@ -1,13 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
+ # An icon attached to a server, tool, prompt, or resource, per the specification's `Icon` type:
5
+ # https://modelcontextprotocol.io/specification/2026-07-28/basic#icons
6
+ #
7
+ # Each argument is checked against the schema's type, as the TypeScript and Python SDKs check theirs,
8
+ # so an icon that would serialize into something a client rejects fails here instead. What a value means
9
+ # is not judged: `src` may be any non-empty `String` (the schema types it as a URI, which an empty `String`
10
+ # is not, and the specification allows an HTTP/HTTPS URL or a `data:` URI), and a size may be any `String`
11
+ # (the specification expects `WxH` or `"any"`).
4
12
  class Icon
5
13
  SUPPORTED_THEMES = ["light", "dark"].freeze
6
14
 
7
15
  attr_reader :mime_type, :sizes, :src, :theme
8
16
 
9
- def initialize(mime_type: nil, sizes: nil, src: nil, theme: nil)
10
- raise ArgumentError, 'The value of theme must specify "light" or "dark".' if theme && !SUPPORTED_THEMES.include?(theme)
17
+ def initialize(mime_type: nil, sizes: nil, src:, theme: nil)
18
+ unless src.is_a?(String) && !src.empty?
19
+ raise ArgumentError, "The value of src must be a non-empty String (got #{src.class})."
20
+ end
21
+
22
+ unless mime_type.nil? || mime_type.is_a?(String)
23
+ raise ArgumentError, "The value of mime_type must be a String (got #{mime_type.class})."
24
+ end
25
+
26
+ if (problem = sizes_problem(sizes))
27
+ raise ArgumentError, "The value of sizes must be an Array of Strings such as [\"48x48\"] or [\"any\"] (#{problem})."
28
+ end
29
+
30
+ unless theme.nil? || SUPPORTED_THEMES.include?(theme)
31
+ raise ArgumentError, 'The value of theme must specify "light" or "dark".'
32
+ end
11
33
 
12
34
  @mime_type = mime_type
13
35
  @sizes = sizes
@@ -18,5 +40,16 @@ module MCP
18
40
  def to_h
19
41
  { mimeType: mime_type, sizes: sizes, src: src, theme: theme }.compact
20
42
  end
43
+
44
+ private
45
+
46
+ def sizes_problem(sizes)
47
+ return if sizes.nil?
48
+ return "got #{sizes.class}" unless sizes.is_a?(Array)
49
+
50
+ index = sizes.index { |size| !size.is_a?(String) }
51
+
52
+ "got #{sizes[index].class} inside the Array" if index
53
+ end
21
54
  end
22
55
  end
@@ -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/tool.rb CHANGED
@@ -149,7 +149,7 @@ module MCP
149
149
  raise ArgumentError, "Tool names should be between 1 and 128 characters in length (inclusive)."
150
150
  end
151
151
 
152
- unless tool_name.match?(/\A[A-Za-z\d_\-\.]+\z/)
152
+ unless tool_name.match?(/\A[A-Za-z\d_\-.]+\z/)
153
153
  raise ArgumentError, <<~MESSAGE
154
154
  Tool names only allowed characters: uppercase and lowercase ASCII letters (A-Z, a-z), digits (0-9), underscore (_), hyphen (-), and dot (.).
155
155
  MESSAGE
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.5.1"
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.5.1
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.5.1
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
@@ -127,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
127
  - !ruby/object:Gem::Version
128
128
  version: '0'
129
129
  requirements: []
130
- rubygems_version: 4.0.16
130
+ rubygems_version: 4.0.20
131
131
  specification_version: 4
132
132
  summary: The official Ruby SDK for Model Context Protocol servers and clients
133
133
  test_files: []