mcp 0.18.0 → 0.19.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/README.md +14 -0
- data/lib/mcp/client/oauth/discovery.rb +44 -0
- data/lib/mcp/client/oauth/flow.rb +83 -2
- data/lib/mcp/client/oauth/provider.rb +30 -2
- data/lib/mcp/client.rb +25 -60
- data/lib/mcp/resource.rb +4 -2
- data/lib/mcp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 959e38a26541c9681c11e8bc227e84c42d20e3d98de1568c0afd45c80f3b4474
|
|
4
|
+
data.tar.gz: e2bf6b0c7926e1803f9dff1279c1f5a35d96f1cf8a1e3c6141b22a97871c8ec2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f347a7259f4e728df5edad217d6963cb3428188e46244efd9be9908e84bf4a7134600c6f453249eedc87e33f998f3f51811922659f116811251a64d78f1bbee
|
|
7
|
+
data.tar.gz: 3d8d25f65c590d07157db74d87423bbde82250b5d99cf2e3182c8adbc198f534f96809265c8615160f49f963a3b2dc2a07a85db2f6f2cc83e11b068583f405de
|
data/README.md
CHANGED
|
@@ -1913,6 +1913,9 @@ pass an `MCP::Client::OAuth::Provider` to the transport instead of a static `Aut
|
|
|
1913
1913
|
- On a `401 Unauthorized`, parse the `WWW-Authenticate` header, discover the authorization server (Protected Resource Metadata + RFC 8414 Authorization Server Metadata),
|
|
1914
1914
|
perform Dynamic Client Registration if needed, run the OAuth 2.1 Authorization Code flow with PKCE (S256), and retry the failed request with the acquired token.
|
|
1915
1915
|
- On subsequent 401s with a saved `refresh_token`, exchange it at the token endpoint before falling back to the full interactive flow (RFC 6749 Section 6).
|
|
1916
|
+
- Request the `offline_access` scope when `client_metadata[:grant_types]` includes `refresh_token` and the authorization server advertises `offline_access` in its metadata
|
|
1917
|
+
`scopes_supported` (SEP-2207). This is what lets the server issue the `refresh_token` used above. As an SDK-level safeguard, when the authorization server does not advertise
|
|
1918
|
+
`offline_access` the scope is also stripped from any other source (challenge, PRM, or provider-supplied scope) so a server that does not support it never receives it.
|
|
1916
1919
|
|
|
1917
1920
|
```ruby
|
|
1918
1921
|
require "mcp"
|
|
@@ -1958,6 +1961,17 @@ Optional keyword arguments:
|
|
|
1958
1961
|
- `scope`: Space-separated scopes to request when the server's `WWW-Authenticate` does not specify one.
|
|
1959
1962
|
- `storage`: Object responding to `tokens`, `save_tokens(t)`, `client_information`, `save_client_information(info)`. Defaults to `MCP::Client::OAuth::InMemoryStorage`,
|
|
1960
1963
|
which keeps credentials in process memory only.
|
|
1964
|
+
- `client_id_metadata_document_url`: URL where you publish a Client ID Metadata Document
|
|
1965
|
+
(`draft-ietf-oauth-client-id-metadata-document` and the MCP authorization specification).
|
|
1966
|
+
When the authorization server advertises `client_id_metadata_document_supported: true`,
|
|
1967
|
+
the SDK uses this URL as the OAuth `client_id` and skips Dynamic Client Registration.
|
|
1968
|
+
Spec-required: the URL MUST be `https://` with a non-root path and MUST NOT include a fragment,
|
|
1969
|
+
userinfo, or `.`/`..` segments. The SDK additionally rejects query strings (the draft only marks
|
|
1970
|
+
them SHOULD NOT include, but the SDK refuses to send any) for `client_id` stability.
|
|
1971
|
+
Any of these failures raise `Provider::InvalidClientIDMetadataDocumentURLError`. The CIMD document
|
|
1972
|
+
served at the URL is a separate JSON artifact from the `client_metadata` keyword above:
|
|
1973
|
+
the DCR `client_metadata` MUST NOT include `client_id`, while the CIMD document MUST include
|
|
1974
|
+
`client_id` set to the document URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
|
|
1961
1975
|
|
|
1962
1976
|
To persist credentials across restarts, supply your own storage:
|
|
1963
1977
|
|
|
@@ -183,6 +183,50 @@ module MCP
|
|
|
183
183
|
false
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
# Returns true when `url` satisfies the structural requirements for
|
|
187
|
+
# a Client ID Metadata Document URL per the MCP 2025-11-25
|
|
188
|
+
# authorization specification and `draft-ietf-oauth-client-id-metadata-document-00`.
|
|
189
|
+
#
|
|
190
|
+
# Spec-required:
|
|
191
|
+
#
|
|
192
|
+
# - scheme MUST be `https` (the loopback-`http` carve-out used for discovery does not apply:
|
|
193
|
+
# the document URL is sent verbatim to the authorization server as the OAuth `client_id`
|
|
194
|
+
# and travels off-loopback)
|
|
195
|
+
# - host MUST be present
|
|
196
|
+
# - path MUST be non-empty and MUST NOT be the root (`/`); the document is a discrete resource,
|
|
197
|
+
# not the origin
|
|
198
|
+
# - URL MUST NOT carry a fragment or userinfo: a fragment is not sent to the server, and userinfo
|
|
199
|
+
# would leak credentials into every `client_id` log line
|
|
200
|
+
# - path MUST be already free of `.` / `..` dot segments after percent-decoding, so two URLs with
|
|
201
|
+
# the same effective path do not produce different `client_id` strings
|
|
202
|
+
#
|
|
203
|
+
# SDK policy (stricter than the draft):
|
|
204
|
+
#
|
|
205
|
+
# - URL MUST NOT carry a query string. The draft marks query components only SHOULD NOT include,
|
|
206
|
+
# but different encodings of the same query (`?a=1&b=2` vs `?b=2&a=1`) would yield distinct
|
|
207
|
+
# `client_id` values for the same logical document.
|
|
208
|
+
def client_id_metadata_document_url?(url)
|
|
209
|
+
return false if url.nil? || url.to_s.empty?
|
|
210
|
+
|
|
211
|
+
uri = URI.parse(url.to_s)
|
|
212
|
+
return false unless uri.scheme&.downcase == "https"
|
|
213
|
+
return false if uri.host.nil? || uri.host.empty?
|
|
214
|
+
return false unless uri.fragment.nil?
|
|
215
|
+
return false unless uri.query.nil?
|
|
216
|
+
return false if uri.respond_to?(:user) && (uri.user || uri.password)
|
|
217
|
+
|
|
218
|
+
path = uri.path.to_s
|
|
219
|
+
return false if path.empty? || path == "/"
|
|
220
|
+
|
|
221
|
+
decoded = path.gsub(/%2[eE]/, ".")
|
|
222
|
+
segments = decoded.split("/", -1)
|
|
223
|
+
return false if segments.any? { |segment| segment == "." || segment == ".." }
|
|
224
|
+
|
|
225
|
+
true
|
|
226
|
+
rescue URI::InvalidURIError
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
|
|
186
230
|
# Like `canonicalize_url` but also strips query string, fragment, and
|
|
187
231
|
# userinfo. This variant is used for identity comparison against
|
|
188
232
|
# the request URL Faraday actually sends, which differs from the value
|
|
@@ -59,6 +59,7 @@ module MCP
|
|
|
59
59
|
client_info = ensure_client_registered(as_metadata: as_metadata)
|
|
60
60
|
|
|
61
61
|
effective_scope = resolve_scope(scope: scope, prm: prm)
|
|
62
|
+
effective_scope = normalize_offline_access_scope(effective_scope, as_metadata: as_metadata)
|
|
62
63
|
pkce = PKCE.generate
|
|
63
64
|
state = SecureRandom.urlsafe_base64(32)
|
|
64
65
|
|
|
@@ -104,8 +105,16 @@ module MCP
|
|
|
104
105
|
refresh_token = read_token("refresh_token")
|
|
105
106
|
raise AuthorizationError, "Cannot refresh: no refresh_token in provider storage." unless refresh_token
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
stored_client_info = @provider.client_information
|
|
109
|
+
have_stored_client_info = stored_client_info.is_a?(Hash) && client_info_required_value(stored_client_info, "client_id")
|
|
110
|
+
|
|
111
|
+
# A CIMD-configured provider stores no `client_information` on purpose
|
|
112
|
+
# (the CIMD URL is re-resolved against the live AS metadata on every flow).
|
|
113
|
+
# Allow refresh to proceed in that case so the `refresh_token` obtained via
|
|
114
|
+
# the CIMD flow remains usable.
|
|
115
|
+
have_cimd_url = !@provider.client_id_metadata_document_url.nil?
|
|
116
|
+
|
|
117
|
+
unless have_stored_client_info || have_cimd_url
|
|
109
118
|
raise AuthorizationError, "Cannot refresh: no client_information in provider storage."
|
|
110
119
|
end
|
|
111
120
|
|
|
@@ -126,6 +135,18 @@ module MCP
|
|
|
126
135
|
ensure_issuer_matches!(expected: authorization_server, returned: as_metadata["issuer"])
|
|
127
136
|
ensure_secure_endpoints!(as_metadata)
|
|
128
137
|
|
|
138
|
+
client_info = if have_stored_client_info
|
|
139
|
+
# Pre-registered / DCR-issued `client_information` always wins: if the user picked an explicit identity,
|
|
140
|
+
# do not silently swap it for the CIMD URL even when the AS also advertises CIMD support.
|
|
141
|
+
stored_client_info
|
|
142
|
+
elsif as_metadata["client_id_metadata_document_supported"] == true
|
|
143
|
+
{ "client_id" => @provider.client_id_metadata_document_url }
|
|
144
|
+
else
|
|
145
|
+
raise AuthorizationError,
|
|
146
|
+
"Cannot refresh: provider has a CIMD URL but the authorization server no longer advertises " \
|
|
147
|
+
"`client_id_metadata_document_supported: true`."
|
|
148
|
+
end
|
|
149
|
+
|
|
129
150
|
new_tokens = exchange_refresh_token(
|
|
130
151
|
as_metadata: as_metadata,
|
|
131
152
|
client_info: client_info,
|
|
@@ -285,6 +306,24 @@ module MCP
|
|
|
285
306
|
existing = @provider.client_information
|
|
286
307
|
return existing if existing.is_a?(Hash) && client_info_required_value(existing, "client_id")
|
|
287
308
|
|
|
309
|
+
# Per the MCP authorization specification and `draft-ietf-oauth-client-id-metadata-document`,
|
|
310
|
+
# if the authorization server advertises Client ID Metadata Document support and the provider has
|
|
311
|
+
# a CIMD URL configured, use the URL as the OAuth `client_id` and skip Dynamic Client Registration.
|
|
312
|
+
#
|
|
313
|
+
# The `== true` comparison is intentional: only a JSON `boolean` `true` opts the flow in.
|
|
314
|
+
# A string `"false"`, an empty Hash, or any other truthy value MUST NOT be treated as CIMD support,
|
|
315
|
+
# otherwise a misconfigured AS could trick the client into using the CIMD `client_id` against
|
|
316
|
+
# a server that has not actually adopted it.
|
|
317
|
+
#
|
|
318
|
+
# The CIMD `client_id` is NOT persisted to storage. The AS may later stop advertising CIMD support
|
|
319
|
+
# (or the operator may rotate the CIMD URL), and a stale `client_information` entry would otherwise
|
|
320
|
+
# keep sending the old CIMD URL forever. Re-evaluating on every flow re-reads the current AS metadata
|
|
321
|
+
# and the current `provider.client_id_metadata_document_url`.
|
|
322
|
+
cimd_url = @provider.client_id_metadata_document_url
|
|
323
|
+
if cimd_url && as_metadata["client_id_metadata_document_supported"] == true
|
|
324
|
+
return { "client_id" => cimd_url }
|
|
325
|
+
end
|
|
326
|
+
|
|
288
327
|
registration_endpoint = as_metadata["registration_endpoint"]
|
|
289
328
|
unless registration_endpoint
|
|
290
329
|
raise AuthorizationError,
|
|
@@ -403,6 +442,48 @@ module MCP
|
|
|
403
442
|
nil
|
|
404
443
|
end
|
|
405
444
|
|
|
445
|
+
# Applies the SDK's `offline_access` policy to the resolved scope. The policy has two halves:
|
|
446
|
+
#
|
|
447
|
+
# - Spec (SEP-2207): a client that wants a refresh token (signalled here by listing
|
|
448
|
+
# `refresh_token` in its registered `grant_types`) MAY request `offline_access`
|
|
449
|
+
# when the authorization server advertises it in metadata `scopes_supported`.
|
|
450
|
+
# When the server advertises it and the client opted in, add it if absent.
|
|
451
|
+
#
|
|
452
|
+
# - SDK policy (defensive hardening): when the server does NOT advertise `offline_access`,
|
|
453
|
+
# strip it from the resolved scope no matter where it came from (the `WWW-Authenticate` challenge,
|
|
454
|
+
# PRM `scopes_supported`, or the provider-supplied scope). SEP-2207 only says clients SHOULD NOT
|
|
455
|
+
# request unsupported scopes, but a misbehaving RS that includes `offline_access` in its challenge,
|
|
456
|
+
# or a misconfigured PRM that lists it under `scopes_supported`, would otherwise propagate into
|
|
457
|
+
# the authorization request even though the AS will not honour it. Stripping here keeps the SDK's
|
|
458
|
+
# own request consistent with the AS's advertisement.
|
|
459
|
+
#
|
|
460
|
+
# Returns `nil` when the result is empty so `build_authorization_url` omits the `scope` parameter entirely.
|
|
461
|
+
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2207
|
|
462
|
+
def normalize_offline_access_scope(scope, as_metadata:)
|
|
463
|
+
scopes = scope.to_s.split
|
|
464
|
+
|
|
465
|
+
if server_supports_offline_access?(as_metadata)
|
|
466
|
+
scopes << "offline_access" if wants_refresh_token? && !scopes.include?("offline_access")
|
|
467
|
+
else
|
|
468
|
+
scopes.delete("offline_access")
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
scopes.empty? ? nil : scopes.join(" ")
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def server_supports_offline_access?(as_metadata)
|
|
475
|
+
supported = as_metadata["scopes_supported"]
|
|
476
|
+
|
|
477
|
+
supported.is_a?(Array) && supported.include?("offline_access")
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def wants_refresh_token?
|
|
481
|
+
metadata = @provider.client_metadata
|
|
482
|
+
grant_types = metadata[:grant_types] || metadata["grant_types"]
|
|
483
|
+
|
|
484
|
+
Array(grant_types).include?("refresh_token")
|
|
485
|
+
end
|
|
486
|
+
|
|
406
487
|
def build_authorization_url(as_metadata:, client_id:, scope:, state:, code_challenge:, resource:)
|
|
407
488
|
authorization_endpoint = as_metadata["authorization_endpoint"]
|
|
408
489
|
unless authorization_endpoint
|
|
@@ -25,6 +25,16 @@ module MCP
|
|
|
25
25
|
# - `storage` - Object responding to `tokens`, `save_tokens(tokens)`,
|
|
26
26
|
# `client_information`, and `save_client_information(info)`. Defaults to
|
|
27
27
|
# an `InMemoryStorage`.
|
|
28
|
+
# - `client_id_metadata_document_url` - URL where the client publishes its Client ID Metadata Document
|
|
29
|
+
# (`draft-ietf-oauth-client-id-metadata-document-00` and the MCP authorization specification).
|
|
30
|
+
# When the authorization server advertises `client_id_metadata_document_supported: true`,
|
|
31
|
+
# the SDK uses this URL as the OAuth `client_id` and skips Dynamic Client Registration.
|
|
32
|
+
# Spec-required: `https://` scheme, a non-root path, and no fragment, userinfo, or `.`/`..` segments.
|
|
33
|
+
# The SDK additionally refuses to send query strings (the draft marks them only SHOULD NOT include,
|
|
34
|
+
# but different encodings of the same query would yield different `client_id` strings for the same document).
|
|
35
|
+
# The document served at the URL is a separate JSON artifact from the `client_metadata` keyword:
|
|
36
|
+
# DCR `client_metadata` MUST NOT include `client_id`, while the CIMD document MUST include `client_id` set
|
|
37
|
+
# to the URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
|
|
28
38
|
class Provider
|
|
29
39
|
# Raised when `Provider#initialize` is called with a `redirect_uri` that
|
|
30
40
|
# is neither HTTPS nor a loopback `http://` URL, per the MCP
|
|
@@ -38,12 +48,21 @@ module MCP
|
|
|
38
48
|
# runtime; failing at construction surfaces the bug earlier.
|
|
39
49
|
class UnregisteredRedirectURIError < ArgumentError; end
|
|
40
50
|
|
|
51
|
+
# Raised when `client_id_metadata_document_url` is provided but does not meet
|
|
52
|
+
# the structural requirements for a Client ID Metadata Document URL:
|
|
53
|
+
# HTTPS, non-root path, and no fragment, query, userinfo, or `.`/`..` segments.
|
|
54
|
+
# The CIMD URL is sent to the authorization server as the OAuth `client_id`,
|
|
55
|
+
# so the same Communication Security guarantee that protects the redirect URI
|
|
56
|
+
# applies and the value must unambiguously identify the document.
|
|
57
|
+
class InvalidClientIDMetadataDocumentURLError < ArgumentError; end
|
|
58
|
+
|
|
41
59
|
attr_reader :client_metadata,
|
|
42
60
|
:redirect_uri,
|
|
43
61
|
:scope,
|
|
44
62
|
:storage,
|
|
45
63
|
:redirect_handler,
|
|
46
|
-
:callback_handler
|
|
64
|
+
:callback_handler,
|
|
65
|
+
:client_id_metadata_document_url
|
|
47
66
|
|
|
48
67
|
def initialize(
|
|
49
68
|
client_metadata:,
|
|
@@ -51,7 +70,8 @@ module MCP
|
|
|
51
70
|
redirect_handler:,
|
|
52
71
|
callback_handler:,
|
|
53
72
|
scope: nil,
|
|
54
|
-
storage: nil
|
|
73
|
+
storage: nil,
|
|
74
|
+
client_id_metadata_document_url: nil
|
|
55
75
|
)
|
|
56
76
|
unless Discovery.secure_url?(redirect_uri)
|
|
57
77
|
raise InsecureRedirectURIError,
|
|
@@ -66,12 +86,20 @@ module MCP
|
|
|
66
86
|
"(got #{registered.inspect}); otherwise the authorization server will reject the authorization request."
|
|
67
87
|
end
|
|
68
88
|
|
|
89
|
+
if client_id_metadata_document_url && !Discovery.client_id_metadata_document_url?(client_id_metadata_document_url)
|
|
90
|
+
raise InvalidClientIDMetadataDocumentURLError,
|
|
91
|
+
"client_id_metadata_document_url #{client_id_metadata_document_url.inspect} must be an https URL " \
|
|
92
|
+
"with a non-root path and no fragment, query, userinfo, or `.`/`..` segments, " \
|
|
93
|
+
"per the MCP authorization specification and `draft-ietf-oauth-client-id-metadata-document`."
|
|
94
|
+
end
|
|
95
|
+
|
|
69
96
|
@client_metadata = client_metadata
|
|
70
97
|
@redirect_uri = redirect_uri
|
|
71
98
|
@redirect_handler = redirect_handler
|
|
72
99
|
@callback_handler = callback_handler
|
|
73
100
|
@scope = scope
|
|
74
101
|
@storage = storage || InMemoryStorage.new
|
|
102
|
+
@client_id_metadata_document_url = client_id_metadata_document_url
|
|
75
103
|
end
|
|
76
104
|
|
|
77
105
|
def access_token
|
data/lib/mcp/client.rb
CHANGED
|
@@ -146,21 +146,7 @@ module MCP
|
|
|
146
146
|
# end
|
|
147
147
|
def tools
|
|
148
148
|
# TODO: consider renaming to `list_all_tools`.
|
|
149
|
-
|
|
150
|
-
seen = Set.new
|
|
151
|
-
cursor = nil
|
|
152
|
-
|
|
153
|
-
loop do
|
|
154
|
-
page = list_tools(cursor: cursor)
|
|
155
|
-
all_tools.concat(page.tools)
|
|
156
|
-
next_cursor = page.next_cursor
|
|
157
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
158
|
-
|
|
159
|
-
seen << next_cursor
|
|
160
|
-
cursor = next_cursor
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
all_tools
|
|
149
|
+
fetch_all_pages { |cursor| list_tools(cursor: cursor) }.flat_map(&:tools)
|
|
164
150
|
end
|
|
165
151
|
|
|
166
152
|
# Returns a single page of resources from the server.
|
|
@@ -189,21 +175,7 @@ module MCP
|
|
|
189
175
|
# @return [Array<Hash>] An array of available resources.
|
|
190
176
|
def resources
|
|
191
177
|
# TODO: consider renaming to `list_all_resources`.
|
|
192
|
-
|
|
193
|
-
seen = Set.new
|
|
194
|
-
cursor = nil
|
|
195
|
-
|
|
196
|
-
loop do
|
|
197
|
-
page = list_resources(cursor: cursor)
|
|
198
|
-
all_resources.concat(page.resources)
|
|
199
|
-
next_cursor = page.next_cursor
|
|
200
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
201
|
-
|
|
202
|
-
seen << next_cursor
|
|
203
|
-
cursor = next_cursor
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
all_resources
|
|
178
|
+
fetch_all_pages { |cursor| list_resources(cursor: cursor) }.flat_map(&:resources)
|
|
207
179
|
end
|
|
208
180
|
|
|
209
181
|
# Returns a single page of resource templates from the server.
|
|
@@ -232,21 +204,7 @@ module MCP
|
|
|
232
204
|
# @return [Array<Hash>] An array of available resource templates.
|
|
233
205
|
def resource_templates
|
|
234
206
|
# TODO: consider renaming to `list_all_resource_templates`.
|
|
235
|
-
|
|
236
|
-
seen = Set.new
|
|
237
|
-
cursor = nil
|
|
238
|
-
|
|
239
|
-
loop do
|
|
240
|
-
page = list_resource_templates(cursor: cursor)
|
|
241
|
-
all_templates.concat(page.resource_templates)
|
|
242
|
-
next_cursor = page.next_cursor
|
|
243
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
244
|
-
|
|
245
|
-
seen << next_cursor
|
|
246
|
-
cursor = next_cursor
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
all_templates
|
|
207
|
+
fetch_all_pages { |cursor| list_resource_templates(cursor: cursor) }.flat_map(&:resource_templates)
|
|
250
208
|
end
|
|
251
209
|
|
|
252
210
|
# Returns a single page of prompts from the server.
|
|
@@ -275,21 +233,7 @@ module MCP
|
|
|
275
233
|
# @return [Array<Hash>] An array of available prompts.
|
|
276
234
|
def prompts
|
|
277
235
|
# TODO: consider renaming to `list_all_prompts`.
|
|
278
|
-
|
|
279
|
-
seen = Set.new
|
|
280
|
-
cursor = nil
|
|
281
|
-
|
|
282
|
-
loop do
|
|
283
|
-
page = list_prompts(cursor: cursor)
|
|
284
|
-
all_prompts.concat(page.prompts)
|
|
285
|
-
next_cursor = page.next_cursor
|
|
286
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
287
|
-
|
|
288
|
-
seen << next_cursor
|
|
289
|
-
cursor = next_cursor
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
all_prompts
|
|
236
|
+
fetch_all_pages { |cursor| list_prompts(cursor: cursor) }.flat_map(&:prompts)
|
|
293
237
|
end
|
|
294
238
|
|
|
295
239
|
# Calls a tool via the transport layer and returns the full response from the server.
|
|
@@ -380,6 +324,27 @@ module MCP
|
|
|
380
324
|
|
|
381
325
|
private
|
|
382
326
|
|
|
327
|
+
# Walks every page of a list endpoint, following `next_cursor`, and returns
|
|
328
|
+
# the page results. The `seen` set guards against a server that repeats or
|
|
329
|
+
# cycles cursors, so the loop always terminates.
|
|
330
|
+
def fetch_all_pages
|
|
331
|
+
pages = []
|
|
332
|
+
seen = Set.new
|
|
333
|
+
cursor = nil
|
|
334
|
+
|
|
335
|
+
loop do
|
|
336
|
+
page = yield(cursor)
|
|
337
|
+
pages << page
|
|
338
|
+
next_cursor = page.next_cursor
|
|
339
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
340
|
+
|
|
341
|
+
seen << next_cursor
|
|
342
|
+
cursor = next_cursor
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
pages
|
|
346
|
+
end
|
|
347
|
+
|
|
383
348
|
def request(method:, params: nil)
|
|
384
349
|
request_body = {
|
|
385
350
|
jsonrpc: JsonRpcHandler::Version::V2_0,
|
data/lib/mcp/resource.rb
CHANGED
|
@@ -5,15 +5,16 @@ require_relative "resource/embedded"
|
|
|
5
5
|
|
|
6
6
|
module MCP
|
|
7
7
|
class Resource
|
|
8
|
-
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta
|
|
8
|
+
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :size, :meta
|
|
9
9
|
|
|
10
|
-
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
10
|
+
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, size: nil, meta: nil)
|
|
11
11
|
@uri = uri
|
|
12
12
|
@name = name
|
|
13
13
|
@title = title
|
|
14
14
|
@description = description
|
|
15
15
|
@icons = icons
|
|
16
16
|
@mime_type = mime_type
|
|
17
|
+
@size = size
|
|
17
18
|
@meta = meta
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -25,6 +26,7 @@ module MCP
|
|
|
25
26
|
description: description,
|
|
26
27
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
27
28
|
mimeType: mime_type,
|
|
29
|
+
size: size,
|
|
28
30
|
_meta: meta,
|
|
29
31
|
}.compact
|
|
30
32
|
end
|
data/lib/mcp/version.rb
CHANGED
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: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Model Context Protocol
|
|
@@ -87,7 +87,7 @@ licenses:
|
|
|
87
87
|
- Apache-2.0
|
|
88
88
|
metadata:
|
|
89
89
|
allowed_push_host: https://rubygems.org
|
|
90
|
-
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.
|
|
90
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.19.0
|
|
91
91
|
homepage_uri: https://ruby.sdk.modelcontextprotocol.io
|
|
92
92
|
source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
93
93
|
bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
|