mcp 1.4.0 → 1.5.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/lib/mcp/client/http.rb +13 -0
- data/lib/mcp/client/oauth/authorization_request.rb +27 -0
- data/lib/mcp/client/oauth/client_credentials_provider.rb +3 -1
- data/lib/mcp/client/oauth/cross_app_access_provider.rb +2 -1
- data/lib/mcp/client/oauth/flow.rb +100 -11
- data/lib/mcp/client/oauth/provider.rb +3 -1
- data/lib/mcp/client/oauth/storage_backed_provider.rb +7 -0
- data/lib/mcp/client/oauth.rb +1 -0
- data/lib/mcp/client/stdio.rb +28 -0
- data/lib/mcp/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19daecce4ccef4ae391f54643fc8fca17360af39e58541404d090dab52858db8
|
|
4
|
+
data.tar.gz: 75f1cac8a1b8c89b6a6a067b763751f39184e1846de5cfe317b3f8e3886fc5a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1653c778f8e782c1ed7c4b66c538076fdce5f35fa00af054f1c9d06c1e78335a02e91734cc90d6107f8ae089946d5085d56d6fc2eeb27a11c84cc6e3f663737
|
|
7
|
+
data.tar.gz: 29cf1b6ee37eed02b83745ce09731db507f64cb95dcb8376ad154525415807db717c8aebd96257c8261eda19b677317a719e8fbd37e0acf3d55e7d8aa1fd8883
|
data/lib/mcp/client/http.rb
CHANGED
|
@@ -1118,6 +1118,19 @@ module MCP
|
|
|
1118
1118
|
},
|
|
1119
1119
|
}
|
|
1120
1120
|
end
|
|
1121
|
+
elsif message["method"] == MCP::Methods::PING && (message["id"].is_a?(String) || message["id"].is_a?(Numeric))
|
|
1122
|
+
# The ping receiver "MUST respond promptly with an empty response" (MCP ping utility),
|
|
1123
|
+
# so an unhandled ping is answered with the empty result rather than Method not found.
|
|
1124
|
+
# A JSON-RPC id is a String or a Number; the reference SDKs reject other shapes at
|
|
1125
|
+
# schema validation, so a ping carrying one falls through to Method not found instead.
|
|
1126
|
+
# The default lives here instead of in `@server_request_handlers`, whose `any?` opens
|
|
1127
|
+
# the GET listening stream on connect: a built-in pong must not change listener behavior.
|
|
1128
|
+
# A handler registered via `on_server_request("ping")` still wins through the branch above.
|
|
1129
|
+
{
|
|
1130
|
+
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
1131
|
+
id: message["id"],
|
|
1132
|
+
result: {},
|
|
1133
|
+
}
|
|
1121
1134
|
else
|
|
1122
1135
|
{
|
|
1123
1136
|
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Client
|
|
5
|
+
module OAuth
|
|
6
|
+
# The decision an `authorization_request_validator` is asked to approve: which authorization server this client is about to send the user to,
|
|
7
|
+
# and with which scopes.
|
|
8
|
+
#
|
|
9
|
+
# Handed over as one object rather than as keyword arguments so that later revisions of the MCP authorization specification can add to it
|
|
10
|
+
# without changing the shape every implementer wrote.
|
|
11
|
+
#
|
|
12
|
+
# Only the named readers are the contract. The positional access `Struct` also provides (`request[0]`, `to_a`, `each`) is not,
|
|
13
|
+
# so the representation can change later.
|
|
14
|
+
AuthorizationRequest = Struct.new(
|
|
15
|
+
# `issuer` of the authorization server selected from the MCP server's Protected Resource Metadata.
|
|
16
|
+
:authorization_server,
|
|
17
|
+
# The scopes about to be requested, as an Array. Chosen by the MCP server, through the `WWW-Authenticate` challenge or `scopes_supported`.
|
|
18
|
+
:scopes,
|
|
19
|
+
# The MCP server URL this client was configured with.
|
|
20
|
+
:server_url,
|
|
21
|
+
# The RFC 8707 `resource` the token is being requested for.
|
|
22
|
+
:resource,
|
|
23
|
+
keyword_init: true,
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -60,7 +60,8 @@ module MCP
|
|
|
60
60
|
private_key: nil,
|
|
61
61
|
signing_algorithm: nil,
|
|
62
62
|
scope: nil,
|
|
63
|
-
storage: nil
|
|
63
|
+
storage: nil,
|
|
64
|
+
authorization_request_validator: nil
|
|
64
65
|
)
|
|
65
66
|
if blank?(client_id)
|
|
66
67
|
raise InvalidCredentialsError, "client_id is required for the client_credentials grant."
|
|
@@ -103,6 +104,7 @@ module MCP
|
|
|
103
104
|
@signing_algorithm = signing_algorithm
|
|
104
105
|
@scope = scope
|
|
105
106
|
@storage = storage || InMemoryStorage.new
|
|
107
|
+
@authorization_request_validator = authorization_request_validator
|
|
106
108
|
@storage.save_client_information(client_information)
|
|
107
109
|
end
|
|
108
110
|
|
|
@@ -35,7 +35,7 @@ module MCP
|
|
|
35
35
|
|
|
36
36
|
attr_reader :scope, :storage
|
|
37
37
|
|
|
38
|
-
def initialize(client_id:, client_secret:, assertion_provider:, scope: nil, storage: nil)
|
|
38
|
+
def initialize(client_id:, client_secret:, assertion_provider:, scope: nil, storage: nil, authorization_request_validator: nil)
|
|
39
39
|
if blank?(client_id)
|
|
40
40
|
raise InvalidConfigurationError, "client_id is required for the jwt-bearer grant."
|
|
41
41
|
end
|
|
@@ -51,6 +51,7 @@ module MCP
|
|
|
51
51
|
@assertion_provider = assertion_provider
|
|
52
52
|
@scope = scope
|
|
53
53
|
@storage = storage || InMemoryStorage.new
|
|
54
|
+
@authorization_request_validator = authorization_request_validator
|
|
54
55
|
@storage.save_client_information(
|
|
55
56
|
"client_id" => client_id,
|
|
56
57
|
"client_secret" => client_secret,
|
|
@@ -23,6 +23,11 @@ module MCP
|
|
|
23
23
|
# should leave the refresh token intact.
|
|
24
24
|
class InvalidGrantError < AuthorizationError; end
|
|
25
25
|
|
|
26
|
+
# Raised when `authorization_request_validator` returns a falsy value. Separate from its parent
|
|
27
|
+
# so that a caller can tell a refusal by its own policy from a network, discovery,
|
|
28
|
+
# or authorization server metadata failure by rescuing a class rather than by matching the message text.
|
|
29
|
+
class AuthorizationRefusedError < AuthorizationError; end
|
|
30
|
+
|
|
26
31
|
def initialize(provider:, http_client_factory: nil)
|
|
27
32
|
@provider = provider
|
|
28
33
|
@http_client_factory = http_client_factory || -> { default_http_client }
|
|
@@ -63,17 +68,22 @@ module MCP
|
|
|
63
68
|
|
|
64
69
|
case provider_authorization_flow
|
|
65
70
|
when :client_credentials
|
|
66
|
-
return run_client_credentials!(as_metadata: as_metadata, prm: prm, resource: resource, scope: scope)
|
|
71
|
+
return run_client_credentials!(as_metadata: as_metadata, prm: prm, resource: resource, scope: scope, server_url: server_url)
|
|
67
72
|
when :jwt_bearer
|
|
68
|
-
return run_jwt_bearer!(as_metadata: as_metadata, prm: prm, resource: resource, scope: scope)
|
|
73
|
+
return run_jwt_bearer!(as_metadata: as_metadata, prm: prm, resource: resource, scope: scope, server_url: server_url)
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
ensure_pkce_supported!(as_metadata)
|
|
72
77
|
|
|
73
|
-
client_info = ensure_client_registered(as_metadata: as_metadata)
|
|
74
|
-
|
|
75
78
|
effective_scope = resolve_scope(scope: scope, prm: prm || {})
|
|
76
79
|
effective_scope = normalize_offline_access_scope(effective_scope, as_metadata: as_metadata)
|
|
80
|
+
|
|
81
|
+
# Asked before registering, not after: a refusal must not leave this client registered at an authorization server
|
|
82
|
+
# the embedding application has just rejected.
|
|
83
|
+
authorize_request!(as_metadata: as_metadata, scope: effective_scope, server_url: server_url, resource: resource)
|
|
84
|
+
|
|
85
|
+
client_info = ensure_client_registered(as_metadata: as_metadata)
|
|
86
|
+
|
|
77
87
|
pkce = PKCE.generate
|
|
78
88
|
state = SecureRandom.urlsafe_base64(32)
|
|
79
89
|
|
|
@@ -109,7 +119,7 @@ module MCP
|
|
|
109
119
|
resource: resource,
|
|
110
120
|
)
|
|
111
121
|
|
|
112
|
-
|
|
122
|
+
save_tokens_issued_by(tokens, as_metadata: as_metadata)
|
|
113
123
|
:authorized
|
|
114
124
|
end
|
|
115
125
|
|
|
@@ -119,17 +129,19 @@ module MCP
|
|
|
119
129
|
# and no `offline_access` augmentation because the grant does not issue a refresh token (OAuth 2.1 Section 4.3.3).
|
|
120
130
|
# The pre-registered `client_id` / `client_secret` come from the provider's stored `client_information`.
|
|
121
131
|
# https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
|
|
122
|
-
def run_client_credentials!(as_metadata:, prm:, resource:, scope:)
|
|
132
|
+
def run_client_credentials!(as_metadata:, prm:, resource:, scope:, server_url:)
|
|
123
133
|
client_info = client_credentials_client_info
|
|
124
134
|
ensure_client_credentials_issuer!(client_info, as_metadata: as_metadata)
|
|
125
135
|
|
|
126
136
|
form = { "grant_type" => "client_credentials" }
|
|
127
137
|
effective_scope = resolve_scope(scope: scope, prm: prm)
|
|
138
|
+
authorize_request!(as_metadata: as_metadata, scope: effective_scope, server_url: server_url, resource: resource)
|
|
128
139
|
form["scope"] = effective_scope if effective_scope
|
|
129
140
|
form["resource"] = resource if resource
|
|
130
141
|
|
|
131
142
|
tokens = post_to_token_endpoint(as_metadata: as_metadata, client_info: client_info, form: form)
|
|
132
|
-
|
|
143
|
+
save_tokens_issued_by(tokens, as_metadata: as_metadata)
|
|
144
|
+
|
|
133
145
|
:authorized
|
|
134
146
|
end
|
|
135
147
|
|
|
@@ -173,12 +185,18 @@ module MCP
|
|
|
173
185
|
# and security checks as `run!`; like `client_credentials`, there is no PKCE, redirect, or authorization request.
|
|
174
186
|
# The assertion's audience is the issuer identifier that `ensure_issuer_matches!` validated.
|
|
175
187
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990
|
|
176
|
-
def run_jwt_bearer!(as_metadata:, prm:, resource:, scope:)
|
|
188
|
+
def run_jwt_bearer!(as_metadata:, prm:, resource:, scope:, server_url:)
|
|
177
189
|
client_info = @provider.client_information
|
|
178
190
|
unless client_info.is_a?(Hash) && client_info_required_value(client_info, "client_id")
|
|
179
191
|
raise AuthorizationError, "Cannot run the jwt-bearer grant: the provider has no stored `client_id`."
|
|
180
192
|
end
|
|
181
193
|
|
|
194
|
+
# Asked before the assertion is minted, for the same reason registration waits on the authorization-code grant:
|
|
195
|
+
# obtaining an ID-JAG sends the identity provider an audience of this authorization server, which must not happen once
|
|
196
|
+
# the host has refused it.
|
|
197
|
+
effective_scope = resolve_scope(scope: scope, prm: prm)
|
|
198
|
+
authorize_request!(as_metadata: as_metadata, scope: effective_scope, server_url: server_url, resource: resource)
|
|
199
|
+
|
|
182
200
|
assertion = @provider.jwt_bearer_assertion(audience: as_metadata["issuer"], resource: resource)
|
|
183
201
|
if assertion.nil? || assertion.to_s.empty?
|
|
184
202
|
raise AuthorizationError, "The provider's assertion_provider returned no ID-JAG assertion."
|
|
@@ -188,12 +206,11 @@ module MCP
|
|
|
188
206
|
"grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
189
207
|
"assertion" => assertion,
|
|
190
208
|
}
|
|
191
|
-
effective_scope = resolve_scope(scope: scope, prm: prm)
|
|
192
209
|
form["scope"] = effective_scope if effective_scope
|
|
193
210
|
form["resource"] = resource if resource
|
|
194
211
|
|
|
195
212
|
tokens = post_to_token_endpoint(as_metadata: as_metadata, client_info: client_info, form: form)
|
|
196
|
-
|
|
213
|
+
save_tokens_issued_by(tokens, as_metadata: as_metadata)
|
|
197
214
|
:authorized
|
|
198
215
|
end
|
|
199
216
|
|
|
@@ -242,6 +259,8 @@ module MCP
|
|
|
242
259
|
server_url: server_url,
|
|
243
260
|
)
|
|
244
261
|
|
|
262
|
+
ensure_token_issuer!(as_metadata: as_metadata)
|
|
263
|
+
|
|
245
264
|
client_info = if have_stored_client_info
|
|
246
265
|
# Pre-registered / DCR-issued `client_information` always wins: if the user picked an explicit identity,
|
|
247
266
|
# do not silently swap it for the CIMD URL even when the AS also advertises CIMD support.
|
|
@@ -262,7 +281,7 @@ module MCP
|
|
|
262
281
|
resource: resource,
|
|
263
282
|
)
|
|
264
283
|
|
|
265
|
-
|
|
284
|
+
save_tokens_issued_by(preserve_refresh_token(new_tokens, refresh_token), as_metadata: as_metadata)
|
|
266
285
|
:refreshed
|
|
267
286
|
end
|
|
268
287
|
|
|
@@ -499,6 +518,43 @@ module MCP
|
|
|
499
518
|
# the authorization server on two loopback ports), and deployments that never leave a corporate network.
|
|
500
519
|
# Only a server reachable on the public internet is barred from steering the client inward.
|
|
501
520
|
# https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
|
|
521
|
+
# Hands the embedding application the authorization server and the scopes that are about to be requested,
|
|
522
|
+
# and abandons the flow when it refuses them.
|
|
523
|
+
#
|
|
524
|
+
# Both values are chosen by the MCP server: it names its own authorization server in Protected
|
|
525
|
+
# Resource Metadata and states the scopes in `scopes_supported` or the `WWW-Authenticate` challenge.
|
|
526
|
+
# Neither the specification nor any MCP SDK binds that choice to the server's own identity,
|
|
527
|
+
# and validating that a token was issued for the intended audience is a responsibility the specification
|
|
528
|
+
# places on MCP servers rather than on clients.
|
|
529
|
+
# A host that knows which providers its user deals with can apply that knowledge here.
|
|
530
|
+
#
|
|
531
|
+
# The scopes are passed on unchanged whatever the host decides, because the specification requires
|
|
532
|
+
# a client to treat the challenged scopes as authoritative for the operation; the choice offered is
|
|
533
|
+
# to proceed or to stop, not to quietly ask for less. A provider without the hook proceeds as before.
|
|
534
|
+
#
|
|
535
|
+
# Only asked when a new grant is being requested. A refresh is not a new grant, and the host already answered
|
|
536
|
+
# this question for that authorization server, so `refresh!` enforces `ensure_token_issuer!` instead:
|
|
537
|
+
# an authorization server that has changed since the tokens were issued sends the flow back through here,
|
|
538
|
+
# where the host sees the new one and decides again.
|
|
539
|
+
def authorize_request!(as_metadata:, scope:, server_url:, resource:)
|
|
540
|
+
return unless @provider.respond_to?(:authorization_request_validator)
|
|
541
|
+
return unless (validator = @provider.authorization_request_validator)
|
|
542
|
+
|
|
543
|
+
request = AuthorizationRequest.new(
|
|
544
|
+
authorization_server: as_metadata["issuer"],
|
|
545
|
+
scopes: scope.to_s.split,
|
|
546
|
+
server_url: server_url,
|
|
547
|
+
resource: resource,
|
|
548
|
+
).freeze
|
|
549
|
+
|
|
550
|
+
return if validator.call(request)
|
|
551
|
+
|
|
552
|
+
raise AuthorizationRefusedError, <<~MESSAGE
|
|
553
|
+
The authorization request was refused by `authorization_request_validator` \
|
|
554
|
+
(authorization server #{request.authorization_server.inspect}, scopes #{request.scopes.inspect}).
|
|
555
|
+
MESSAGE
|
|
556
|
+
end
|
|
557
|
+
|
|
502
558
|
def ensure_routable_destination!(url, label:, server_url:)
|
|
503
559
|
return unless private_network_url?(url)
|
|
504
560
|
return if private_network_url?(server_url)
|
|
@@ -669,6 +725,39 @@ module MCP
|
|
|
669
725
|
# the error and falls back to the full flow, which discards the stale registration and re-registers.
|
|
670
726
|
# Credentials without an `"issuer"` binding predate this check and are allowed through;
|
|
671
727
|
# CIMD `client_id`s are portable.
|
|
728
|
+
# Records which authorization server minted these tokens, alongside the tokens themselves.
|
|
729
|
+
# SEP-2352 already binds *client credentials* to their issuer; this binds the *tokens*, which that
|
|
730
|
+
# SEP leaves unbound.
|
|
731
|
+
# Stored in the token hash so it travels through any `storage` a caller supplied, without widening
|
|
732
|
+
# the storage interface that custom implementations have to satisfy.
|
|
733
|
+
def save_tokens_issued_by(tokens, as_metadata:)
|
|
734
|
+
issuer = as_metadata["issuer"]
|
|
735
|
+
tokens = tokens.merge("issuer" => issuer) if tokens.is_a?(Hash) && issuer
|
|
736
|
+
|
|
737
|
+
@provider.save_tokens(tokens)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# Refuses to present a refresh token to an authorization server other than the one that issued it.
|
|
741
|
+
# `refresh!` rediscovers the authorization server from Protected Resource Metadata every time,
|
|
742
|
+
# so the server named for a session can differ from the one the stored tokens came from.
|
|
743
|
+
# Unlike `ensure_refreshable_client_information!` this holds whatever the client identity is,
|
|
744
|
+
# including a Client ID Metadata Document URL, which is portable across authorization servers precisely
|
|
745
|
+
# so that re-registration is unnecessary.
|
|
746
|
+
#
|
|
747
|
+
# Tokens stored before this shipped carry no issuer and are left alone, matching how SEP-2352 treats
|
|
748
|
+
# client information without one; the binding takes effect from their next authorization.
|
|
749
|
+
# The caller answers a refusal by running the full authorization flow, which is where the embedding application
|
|
750
|
+
# is asked about the new authorization server.
|
|
751
|
+
def ensure_token_issuer!(as_metadata:)
|
|
752
|
+
stored_issuer = read_token("issuer")
|
|
753
|
+
return if stored_issuer.nil? || stored_issuer == as_metadata["issuer"]
|
|
754
|
+
|
|
755
|
+
raise AuthorizationError, <<~MESSAGE
|
|
756
|
+
Cannot refresh: the stored tokens were issued by a different authorization server (stored issuer #{stored_issuer.inspect}, \
|
|
757
|
+
current #{as_metadata["issuer"].inspect}); re-authorization is required.
|
|
758
|
+
MESSAGE
|
|
759
|
+
end
|
|
760
|
+
|
|
672
761
|
def ensure_refreshable_client_information!(client_info, as_metadata:)
|
|
673
762
|
stored_issuer = client_info_required_value(client_info, "issuer")
|
|
674
763
|
return if stored_issuer.nil?
|
|
@@ -86,7 +86,8 @@ module MCP
|
|
|
86
86
|
callback_handler:,
|
|
87
87
|
scope: nil,
|
|
88
88
|
storage: nil,
|
|
89
|
-
client_id_metadata_document_url: nil
|
|
89
|
+
client_id_metadata_document_url: nil,
|
|
90
|
+
authorization_request_validator: nil
|
|
90
91
|
)
|
|
91
92
|
unless Discovery.secure_url?(redirect_uri)
|
|
92
93
|
raise InsecureRedirectURIError,
|
|
@@ -115,6 +116,7 @@ module MCP
|
|
|
115
116
|
@scope = scope
|
|
116
117
|
@storage = storage || InMemoryStorage.new
|
|
117
118
|
@client_id_metadata_document_url = client_id_metadata_document_url
|
|
119
|
+
@authorization_request_validator = authorization_request_validator
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
# Identifies the OAuth flow this provider drives.
|
|
@@ -14,6 +14,13 @@ module MCP
|
|
|
14
14
|
# `save_tokens(tokens)`, `client_information`, and `save_client_information(info)`
|
|
15
15
|
# (see `InMemoryStorage`).
|
|
16
16
|
module StorageBackedProvider
|
|
17
|
+
# Optional `->(request) { true | false }` hook, called with an `AuthorizationRequest`.
|
|
18
|
+
# An MCP server names its own authorization server and the scopes to ask for, so this is where
|
|
19
|
+
# an embedding application sees both before the request is made and can decline to proceed.
|
|
20
|
+
# `nil` (the default) authorizes whatever the server asked for, which is the behavior every
|
|
21
|
+
# MCP SDK has today.
|
|
22
|
+
attr_reader :authorization_request_validator
|
|
23
|
+
|
|
17
24
|
def access_token
|
|
18
25
|
tokens&.dig("access_token") || tokens&.dig(:access_token)
|
|
19
26
|
end
|
data/lib/mcp/client/oauth.rb
CHANGED
data/lib/mcp/client/stdio.rb
CHANGED
|
@@ -436,6 +436,18 @@ module MCP
|
|
|
436
436
|
|
|
437
437
|
parsed = JSON.parse(line.strip)
|
|
438
438
|
|
|
439
|
+
# A frame carrying `method` is a request or notification, never the awaited response,
|
|
440
|
+
# whatever its id says: JSON-RPC id spaces are per sender, so a server-chosen ping id
|
|
441
|
+
# may legitimately collide with the awaited one. Pings are answered;
|
|
442
|
+
# other server-to-client requests over stdio stay unsupported as documented,
|
|
443
|
+
# and notifications carry no id.
|
|
444
|
+
if parsed.is_a?(Hash) && parsed.key?("method")
|
|
445
|
+
# A JSON-RPC id is a String or a Number; the reference SDKs reject other shapes at
|
|
446
|
+
# schema validation, so a ping carrying one is skipped rather than echoed back.
|
|
447
|
+
answer_ping(parsed) if parsed["method"] == MCP::Methods::PING && json_rpc_id?(parsed["id"])
|
|
448
|
+
next
|
|
449
|
+
end
|
|
450
|
+
|
|
439
451
|
# A JSON-RPC message is an object; skip a non-object frame (array or scalar)
|
|
440
452
|
# the same way as a frame without an id.
|
|
441
453
|
next unless parsed.is_a?(Hash) && parsed.key?("id")
|
|
@@ -451,6 +463,22 @@ module MCP
|
|
|
451
463
|
)
|
|
452
464
|
end
|
|
453
465
|
|
|
466
|
+
# The ping receiver "MUST respond promptly with an empty response". Answered inline from
|
|
467
|
+
# the read loop, so a keepalive cannot stall unanswered while a response is awaited;
|
|
468
|
+
# between requests nothing reads stdout, and a queued ping is answered on the next read.
|
|
469
|
+
def answer_ping(parsed)
|
|
470
|
+
@write_mutex.synchronize do
|
|
471
|
+
write_message({ jsonrpc: JsonRpcHandler::Version::V2_0, id: parsed["id"], result: {} })
|
|
472
|
+
end
|
|
473
|
+
rescue RequestHandlerError
|
|
474
|
+
# Best effort: a pong cannot be delivered over a broken stdin, and the failure must not
|
|
475
|
+
# surface as an error of the unrelated request whose response the loop is reading.
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def json_rpc_id?(id)
|
|
479
|
+
id.is_a?(String) || id.is_a?(Numeric)
|
|
480
|
+
end
|
|
481
|
+
|
|
454
482
|
def ensure_running!
|
|
455
483
|
return if @wait_thread.alive?
|
|
456
484
|
|
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: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Model Context Protocol
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/mcp/client/mcp_param_headers.rb
|
|
47
47
|
- lib/mcp/client/modern_envelope.rb
|
|
48
48
|
- lib/mcp/client/oauth.rb
|
|
49
|
+
- lib/mcp/client/oauth/authorization_request.rb
|
|
49
50
|
- lib/mcp/client/oauth/bounded_body.rb
|
|
50
51
|
- lib/mcp/client/oauth/client_credentials_provider.rb
|
|
51
52
|
- lib/mcp/client/oauth/cross_app_access_provider.rb
|
|
@@ -107,7 +108,7 @@ licenses:
|
|
|
107
108
|
- Apache-2.0
|
|
108
109
|
metadata:
|
|
109
110
|
allowed_push_host: https://rubygems.org
|
|
110
|
-
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.
|
|
111
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.5.0
|
|
111
112
|
homepage_uri: https://ruby.sdk.modelcontextprotocol.io
|
|
112
113
|
source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
113
114
|
bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
|