mcp 0.19.0 → 0.20.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 +78 -3
- data/lib/json_rpc_handler.rb +7 -2
- data/lib/mcp/client/http.rb +68 -7
- data/lib/mcp/client/oauth/client_credentials_provider.rb +89 -0
- data/lib/mcp/client/oauth/discovery.rb +11 -1
- data/lib/mcp/client/oauth/flow.rb +57 -9
- data/lib/mcp/client/oauth/provider.rb +12 -25
- data/lib/mcp/client/oauth/storage_backed_provider.rb +43 -0
- data/lib/mcp/client/oauth.rb +3 -1
- data/lib/mcp/client.rb +46 -19
- data/lib/mcp/configuration.rb +1 -0
- data/lib/mcp/resource.rb +4 -2
- data/lib/mcp/resource_template.rb +4 -2
- data/lib/mcp/server/transports/streamable_http_transport.rb +51 -31
- data/lib/mcp/server.rb +27 -1
- data/lib/mcp/tool/input_schema.rb +2 -2
- data/lib/mcp/tool/schema.rb +38 -19
- data/lib/mcp/trace_context.rb +23 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +1 -0
- metadata +8 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1915f5e50dda558f3ba32d0c946dacafda7c0ee183300cd634632505ab206bd
|
|
4
|
+
data.tar.gz: 9f702e675c56e191effefa6a7a7ac5b4d5f3c0c8bded9284484bf7260b551f3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76797292c1345c02d77b63b5d840bf9a39fe719e2df6a232a32a342d9e6974e296bc4e1a08939bba539ea56f9a00ab25cf225936ecd02cd1cee31fdfd7e39d48
|
|
7
|
+
data.tar.gz: f1063ce3c1781e713f556489b0a70d58d102b42d6f220b63a138b97a0532dbfd521b7aea428ed2811d94923bff39ab6f02cad0504ad0597c53e48676cb27984f
|
data/README.md
CHANGED
|
@@ -47,6 +47,7 @@ It implements the Model Context Protocol specification, handling model context r
|
|
|
47
47
|
|
|
48
48
|
- `initialize` - Initializes the protocol and returns server capabilities
|
|
49
49
|
- `ping` - Simple health check
|
|
50
|
+
- `logging/setLevel` - Configures the minimum log level for the server
|
|
50
51
|
- `tools/list` - Lists all registered tools and their schemas
|
|
51
52
|
- `tools/call` - Invokes a specific tool with provided arguments
|
|
52
53
|
- `prompts/list` - Lists all registered prompts and their schemas
|
|
@@ -260,6 +261,11 @@ See the relevant sections below for the arguments they receive.
|
|
|
260
261
|
|
|
261
262
|
The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/2025-06-18/basic#general-fields) in requests that allows clients to pass request-specific metadata. The server automatically extracts this parameter and makes it available to tools and prompts as a nested field within the `server_context`.
|
|
262
263
|
|
|
264
|
+
> [!NOTE]
|
|
265
|
+
> `_meta` is only merged when `server_context` is a `Hash` (or `nil`, in which case a new `{ _meta: ... }` hash is synthesized).
|
|
266
|
+
> If you assign a non-`Hash` value to `server_context`, `_meta` is not merged and tools will not see it
|
|
267
|
+
> under `server_context[:_meta]`. Keep `server_context` as a `Hash` if your tools need access to `_meta`.
|
|
268
|
+
|
|
263
269
|
**Access Pattern:**
|
|
264
270
|
|
|
265
271
|
When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
|
|
@@ -300,6 +306,36 @@ end
|
|
|
300
306
|
}
|
|
301
307
|
```
|
|
302
308
|
|
|
309
|
+
**Distributed Tracing (W3C Trace Context):**
|
|
310
|
+
|
|
311
|
+
Per SEP-414, the keys `traceparent`, `tracestate`, and `baggage` are reserved un-prefixed `_meta` keys for propagating
|
|
312
|
+
[W3C Trace Context](https://www.w3.org/TR/trace-context/) across MCP requests. The SDK guarantees these keys pass through
|
|
313
|
+
incoming request `_meta` untouched, and exposes their names as constants on `MCP::TraceContext` (`TRACEPARENT_META_KEY`,
|
|
314
|
+
`TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`, and `META_KEYS`). The SDK does not depend on OpenTelemetry; bridge the values
|
|
315
|
+
to your tracing system yourself:
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
class TracedTool < MCP::Tool
|
|
319
|
+
def self.call(message:, server_context:)
|
|
320
|
+
traceparent = server_context.dig(:_meta, :traceparent)
|
|
321
|
+
# Hand traceparent/tracestate/baggage to your tracing library
|
|
322
|
+
# (e.g. the opentelemetry-ruby gems) to continue the caller's trace.
|
|
323
|
+
|
|
324
|
+
MCP::Tool::Response.new([{ type: "text", text: "ok" }])
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
On the client side, every request method (`call_tool`, `read_resource`, `get_prompt`, `complete`, `ping`, and the `list_*` methods)
|
|
330
|
+
accepts a `meta:` keyword to inject these keys into the outgoing request, so trace context can flow on every request:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
meta = { MCP::TraceContext::TRACEPARENT_META_KEY => "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" }
|
|
334
|
+
|
|
335
|
+
client.call_tool(tool: tool, arguments: { message: "Hello" }, meta: meta)
|
|
336
|
+
client.read_resource(uri: "file:///report.txt", meta: meta)
|
|
337
|
+
```
|
|
338
|
+
|
|
303
339
|
#### Configuration Block Data
|
|
304
340
|
|
|
305
341
|
##### Exception Reporter
|
|
@@ -423,10 +459,10 @@ The exception reporter receives two arguments:
|
|
|
423
459
|
- `exception`: The Ruby exception object that was raised
|
|
424
460
|
- `server_context`: A hash containing contextual information about where the error occurred
|
|
425
461
|
|
|
426
|
-
The server_context hash includes:
|
|
462
|
+
The `server_context` hash includes:
|
|
427
463
|
|
|
428
|
-
- For
|
|
429
|
-
- For
|
|
464
|
+
- For request handling failures: `{ request: { ... } }` (the raw JSON-RPC request hash)
|
|
465
|
+
- For notification delivery failures: `{ notification: "tools_list_changed" }` (or the relevant notification name)
|
|
430
466
|
|
|
431
467
|
When an exception occurs:
|
|
432
468
|
|
|
@@ -891,6 +927,19 @@ end
|
|
|
891
927
|
|
|
892
928
|
otherwise `resources/read` requests will be a no-op.
|
|
893
929
|
|
|
930
|
+
For unknown URIs, raise `MCP::Server::ResourceNotFoundError` from the handler.
|
|
931
|
+
Per SEP-2164, the server then responds with the standard JSON-RPC Invalid Params error (`-32602`)
|
|
932
|
+
carrying the requested URI in the error `data` member:
|
|
933
|
+
|
|
934
|
+
```ruby
|
|
935
|
+
server.resources_read_handler do |params|
|
|
936
|
+
resource = lookup(params[:uri])
|
|
937
|
+
raise MCP::Server::ResourceNotFoundError.new(params[:uri], params) unless resource
|
|
938
|
+
|
|
939
|
+
[{ uri: params[:uri], mimeType: resource.mime_type, text: resource.body }]
|
|
940
|
+
end
|
|
941
|
+
```
|
|
942
|
+
|
|
894
943
|
### Resource Templates
|
|
895
944
|
|
|
896
945
|
The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
|
|
@@ -1913,6 +1962,9 @@ pass an `MCP::Client::OAuth::Provider` to the transport instead of a static `Aut
|
|
|
1913
1962
|
- On a `401 Unauthorized`, parse the `WWW-Authenticate` header, discover the authorization server (Protected Resource Metadata + RFC 8414 Authorization Server Metadata),
|
|
1914
1963
|
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
1964
|
- 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).
|
|
1965
|
+
- On a `403 Forbidden` whose `WWW-Authenticate` header carries `error="insufficient_scope"` (OAuth 2.0 step-up, RFC 6750 Section 3.1 and the MCP scope-selection-strategy),
|
|
1966
|
+
run a fresh authorization request for the union of the currently granted scope and the scope named in the challenge, then retry the failed request once.
|
|
1967
|
+
The refresh path is bypassed because refreshing would re-issue the same scope set the server just rejected. A `403` without that challenge is surfaced unchanged.
|
|
1916
1968
|
- Request the `offline_access` scope when `client_metadata[:grant_types]` includes `refresh_token` and the authorization server advertises `offline_access` in its metadata
|
|
1917
1969
|
`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
1970
|
`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.
|
|
@@ -2014,6 +2066,29 @@ provider = MCP::Client::OAuth::Provider.new(
|
|
|
2014
2066
|
)
|
|
2015
2067
|
```
|
|
2016
2068
|
|
|
2069
|
+
##### Client Credentials Grant
|
|
2070
|
+
|
|
2071
|
+
For a confidential machine-to-machine client (no user, no browser redirect), use `MCP::Client::OAuth::ClientCredentialsProvider` instead of `Provider`.
|
|
2072
|
+
The transport discovers the authorization server the same way, then exchanges the OAuth 2.1 `client_credentials` grant (RFC 6749 Section 4.4) at
|
|
2073
|
+
the token endpoint. There is no authorization request, PKCE, or `offline_access`, because the grant does not issue a refresh token.
|
|
2074
|
+
|
|
2075
|
+
```ruby
|
|
2076
|
+
provider = MCP::Client::OAuth::ClientCredentialsProvider.new(
|
|
2077
|
+
client_id: "my-service",
|
|
2078
|
+
client_secret: ENV.fetch("MCP_CLIENT_SECRET"),
|
|
2079
|
+
# token_endpoint_auth_method: "client_secret_basic" (default) or "client_secret_post"
|
|
2080
|
+
# scope: "mcp:read mcp:write" (optional; used when the server does not advertise scopes)
|
|
2081
|
+
)
|
|
2082
|
+
|
|
2083
|
+
transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp", oauth: provider)
|
|
2084
|
+
```
|
|
2085
|
+
|
|
2086
|
+
Keyword arguments:
|
|
2087
|
+
|
|
2088
|
+
- `client_id`, `client_secret`: Required. The grant is for confidential clients, so a credential is mandatory.
|
|
2089
|
+
- `token_endpoint_auth_method`: `"client_secret_basic"` (default) or `"client_secret_post"`. `"none"` is rejected with `ClientCredentialsProvider::InvalidCredentialsError`.
|
|
2090
|
+
- `scope`, `storage`: Optional, same meaning as on `Provider`.
|
|
2091
|
+
|
|
2017
2092
|
##### Communication Security
|
|
2018
2093
|
|
|
2019
2094
|
When `oauth:` is set, the MCP transport URL and every OAuth-facing URL (PRM, Authorization Server metadata, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`,
|
data/lib/json_rpc_handler.rb
CHANGED
|
@@ -73,13 +73,18 @@ module JsonRpcHandler
|
|
|
73
73
|
|
|
74
74
|
error = if !valid_version?(request[:jsonrpc])
|
|
75
75
|
"JSON-RPC version must be 2.0"
|
|
76
|
-
elsif !valid_id?(
|
|
76
|
+
elsif !valid_id?(id, id_validation_pattern)
|
|
77
77
|
"Request ID must match validation pattern, or be an integer or null"
|
|
78
78
|
elsif !valid_method_name?(request[:method])
|
|
79
79
|
'Method name must be a string and not start with "rpc."'
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
# Per JSON-RPC 2.0 (Response object, `id`), the error response must carry
|
|
83
|
+
# the same id as the request when the id could be detected; null is only
|
|
84
|
+
# for requests whose id could not be determined. `error_response` nils out
|
|
85
|
+
# ids that fail validation, and the `:unknown_id` sentinel keeps a response
|
|
86
|
+
# (with a null id) being emitted when the request carried no id at all.
|
|
87
|
+
return error_response(id: id.nil? ? :unknown_id : id, id_validation_pattern: id_validation_pattern, error: {
|
|
83
88
|
code: ErrorCode::INVALID_REQUEST,
|
|
84
89
|
message: "Invalid Request",
|
|
85
90
|
data: error,
|
data/lib/mcp/client/http.rb
CHANGED
|
@@ -185,6 +185,7 @@ module MCP
|
|
|
185
185
|
method = request[:method] || request["method"]
|
|
186
186
|
params = request[:params] || request["params"]
|
|
187
187
|
oauth_retried = false
|
|
188
|
+
step_up_retried = false
|
|
188
189
|
|
|
189
190
|
begin
|
|
190
191
|
response = client.post("", request, session_headers)
|
|
@@ -217,6 +218,19 @@ module MCP
|
|
|
217
218
|
original_error: e,
|
|
218
219
|
)
|
|
219
220
|
rescue Faraday::ForbiddenError => e
|
|
221
|
+
# OAuth 2.0 step-up: a 403 carrying `error="insufficient_scope"` in
|
|
222
|
+
# the Bearer challenge means the existing access token is valid
|
|
223
|
+
# but lacks scopes the server now requires for this operation.
|
|
224
|
+
# Re-run the full authorization flow with the escalated scope from
|
|
225
|
+
# the challenge and retry once. A plain 403 without the challenge is
|
|
226
|
+
# surfaced unchanged.
|
|
227
|
+
if @oauth && !step_up_retried && insufficient_scope_challenge?(e)
|
|
228
|
+
step_up_retried = true
|
|
229
|
+
run_step_up_flow!(forbidden_error: e)
|
|
230
|
+
|
|
231
|
+
retry
|
|
232
|
+
end
|
|
233
|
+
|
|
220
234
|
raise RequestHandlerError.new(
|
|
221
235
|
"You are forbidden to make #{method} requests",
|
|
222
236
|
{ method: method, params: params },
|
|
@@ -337,16 +351,63 @@ module MCP
|
|
|
337
351
|
#
|
|
338
352
|
# https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#error-handling
|
|
339
353
|
def run_oauth_flow!(unauthorized_error:)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
params = MCP::Client::OAuth::Discovery.parse_www_authenticate(www_authenticate)
|
|
354
|
+
params = parse_www_authenticate_from_error(unauthorized_error)
|
|
355
|
+
flow = MCP::Client::OAuth::Flow.new(provider: @oauth)
|
|
356
|
+
return if attempt_refresh(flow: flow, resource_metadata_url: params["resource_metadata"])
|
|
344
357
|
|
|
358
|
+
run_full_authorization_flow!(flow: flow, params: params)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Drives a full Authorization Code + PKCE flow without first attempting
|
|
362
|
+
# to refresh the access token. Used for the MCP scope-selection-strategy
|
|
363
|
+
# step-up path: the provider already holds a valid access token,
|
|
364
|
+
# but the server returned a 403 with
|
|
365
|
+
# `WWW-Authenticate: ... error="insufficient_scope", scope="..."`
|
|
366
|
+
# per RFC 6750 Section 3.1. Refreshing the existing token would re-issue
|
|
367
|
+
# the same scope set the server already rejected, so the SDK must run
|
|
368
|
+
# a fresh authorization request. The request asks for the union of
|
|
369
|
+
# the currently granted scope and the newly demanded scope; otherwise
|
|
370
|
+
# the caller would lose previously held scopes and trigger another step-up
|
|
371
|
+
# on the next operation that needs them.
|
|
372
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#scope-selection-strategy
|
|
373
|
+
def run_step_up_flow!(forbidden_error:)
|
|
374
|
+
params = parse_www_authenticate_from_error(forbidden_error)
|
|
345
375
|
flow = MCP::Client::OAuth::Flow.new(provider: @oauth)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
376
|
+
params = params.merge("scope" => escalated_step_up_scope(params["scope"]))
|
|
377
|
+
|
|
378
|
+
run_full_authorization_flow!(flow: flow, params: params)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Returns the space-separated union of the currently granted scope (read
|
|
382
|
+
# from the stored token response per RFC 6749 Section 5.1) and the scope
|
|
383
|
+
# demanded by the step-up challenge. Duplicates are collapsed; order
|
|
384
|
+
# follows first appearance so existing scopes precede the newly added
|
|
385
|
+
# ones. Returns `nil` when neither side carries a scope so
|
|
386
|
+
# `build_authorization_url` omits the `scope` parameter entirely.
|
|
387
|
+
def escalated_step_up_scope(challenge_scope)
|
|
388
|
+
tokens = @oauth.tokens
|
|
389
|
+
granted = tokens.is_a?(Hash) ? (tokens["scope"] || tokens[:scope]) : nil
|
|
390
|
+
scopes = [granted, challenge_scope].compact.flat_map { |scope| scope.to_s.split }.uniq
|
|
391
|
+
|
|
392
|
+
scopes.empty? ? nil : scopes.join(" ")
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# True when the response on `forbidden_error` carries a Bearer challenge
|
|
396
|
+
# with `error="insufficient_scope"` per RFC 6750 Section 3.1 and the MCP
|
|
397
|
+
# scope-selection-strategy section. A 403 without that signal is not a
|
|
398
|
+
# step-up challenge and must not trigger re-authorization.
|
|
399
|
+
def insufficient_scope_challenge?(forbidden_error)
|
|
400
|
+
parse_www_authenticate_from_error(forbidden_error)["error"] == "insufficient_scope"
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def parse_www_authenticate_from_error(error)
|
|
404
|
+
response = error.response || {}
|
|
405
|
+
response_headers = response[:headers] || {}
|
|
406
|
+
header = response_headers["www-authenticate"] || response_headers["WWW-Authenticate"]
|
|
407
|
+
MCP::Client::OAuth::Discovery.parse_www_authenticate(header)
|
|
408
|
+
end
|
|
349
409
|
|
|
410
|
+
def run_full_authorization_flow!(flow:, params:)
|
|
350
411
|
# Use the URL snapshotted at `initialize` time so a post-construction
|
|
351
412
|
# mutation of `@url` cannot redirect PRM/AS discovery and the authorize
|
|
352
413
|
# URL to an attacker-controlled host.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Client
|
|
5
|
+
module OAuth
|
|
6
|
+
# OAuth client configuration for the OAuth 2.1 `client_credentials` grant
|
|
7
|
+
# (machine-to-machine, no user and no browser redirect). Handed to
|
|
8
|
+
# `MCP::Client::HTTP` via the `oauth:` keyword, the same as `Provider`.
|
|
9
|
+
# The interactive Authorization Code flow lives in `Provider`;
|
|
10
|
+
# this class exists so a credentials-only client never has to supply
|
|
11
|
+
# the redirect arguments that grant has no use for, mirroring the dedicated
|
|
12
|
+
# `ClientCredentialsProvider` in the TypeScript SDK and
|
|
13
|
+
# `ClientCredentialsOAuthProvider` in the Python SDK.
|
|
14
|
+
#
|
|
15
|
+
# Required keyword arguments:
|
|
16
|
+
#
|
|
17
|
+
# - `client_id` - String identifying the pre-registered confidential client.
|
|
18
|
+
# - `client_secret` - String shared secret. The `client_credentials` grant
|
|
19
|
+
# is for confidential clients, so a credential is mandatory.
|
|
20
|
+
#
|
|
21
|
+
# Optional keyword arguments:
|
|
22
|
+
#
|
|
23
|
+
# - `token_endpoint_auth_method` - `"client_secret_basic"` (default) or
|
|
24
|
+
# `"client_secret_post"`. `"none"` is rejected: an unauthenticated
|
|
25
|
+
# `client_credentials` request is meaningless.
|
|
26
|
+
# - `scope` - String of space-separated scopes to request when the server's
|
|
27
|
+
# `WWW-Authenticate` and the Protected Resource Metadata do not specify one.
|
|
28
|
+
# - `storage` - Object responding to `tokens`, `save_tokens(tokens)`,
|
|
29
|
+
# `client_information`, and `save_client_information(info)`. Defaults to
|
|
30
|
+
# an `InMemoryStorage`. The `client_id` / `client_secret` are written
|
|
31
|
+
# into it so the token exchange reads them through the same path as
|
|
32
|
+
# a pre-registered authorization-code client.
|
|
33
|
+
class ClientCredentialsProvider
|
|
34
|
+
include StorageBackedProvider
|
|
35
|
+
|
|
36
|
+
# Raised when the credentials required for the `client_credentials` grant are
|
|
37
|
+
# missing or the requested client authentication method cannot carry them.
|
|
38
|
+
class InvalidCredentialsError < ArgumentError; end
|
|
39
|
+
|
|
40
|
+
SUPPORTED_AUTH_METHODS = ["client_secret_basic", "client_secret_post"].freeze
|
|
41
|
+
|
|
42
|
+
attr_reader :scope, :storage
|
|
43
|
+
|
|
44
|
+
def initialize(
|
|
45
|
+
client_id:,
|
|
46
|
+
client_secret:,
|
|
47
|
+
token_endpoint_auth_method: "client_secret_basic",
|
|
48
|
+
scope: nil,
|
|
49
|
+
storage: nil
|
|
50
|
+
)
|
|
51
|
+
if blank?(client_id)
|
|
52
|
+
raise InvalidCredentialsError, "client_id is required for the client_credentials grant."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
unless SUPPORTED_AUTH_METHODS.include?(token_endpoint_auth_method)
|
|
56
|
+
raise InvalidCredentialsError,
|
|
57
|
+
"token_endpoint_auth_method must be one of #{SUPPORTED_AUTH_METHODS.inspect} for the " \
|
|
58
|
+
"client_credentials grant (got #{token_endpoint_auth_method.inspect}); an unauthenticated " \
|
|
59
|
+
"client_credentials request is not allowed."
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if blank?(client_secret)
|
|
63
|
+
raise InvalidCredentialsError,
|
|
64
|
+
"client_secret is required for the client_credentials grant with #{token_endpoint_auth_method}."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@scope = scope
|
|
68
|
+
@storage = storage || InMemoryStorage.new
|
|
69
|
+
@storage.save_client_information(
|
|
70
|
+
"client_id" => client_id,
|
|
71
|
+
"client_secret" => client_secret,
|
|
72
|
+
"token_endpoint_auth_method" => token_endpoint_auth_method,
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# See `Provider#authorization_flow`.
|
|
77
|
+
def authorization_flow
|
|
78
|
+
:client_credentials
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def blank?(value)
|
|
84
|
+
value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -90,7 +90,17 @@ module MCP
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# Returns the candidate Authorization Server metadata URLs to probe, in priority order.
|
|
93
|
-
#
|
|
93
|
+
#
|
|
94
|
+
# Per SEP-2351, MCP uses the default `oauth-authorization-server` well-known URI suffix
|
|
95
|
+
# registered by RFC 8414 Section 7.3 and defines no application-specific suffix of its own.
|
|
96
|
+
# The OAuth candidates below therefore use only that default suffix
|
|
97
|
+
# (plus the `openid-configuration` suffix from OpenID Connect Discovery),
|
|
98
|
+
# both in the RFC 8414 Section 3.1 path-inserted form for issuers with a path component
|
|
99
|
+
# and in the root form for issuers without one.
|
|
100
|
+
#
|
|
101
|
+
# - https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery
|
|
102
|
+
# - https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2351
|
|
103
|
+
# - https://www.rfc-editor.org/rfc/rfc8414#section-3.1
|
|
94
104
|
def authorization_server_metadata_urls(issuer_url)
|
|
95
105
|
uri = URI.parse(issuer_url)
|
|
96
106
|
path = uri.path == "/" ? "" : uri.path.to_s
|
|
@@ -54,6 +54,11 @@ module MCP
|
|
|
54
54
|
as_metadata = fetch_authorization_server_metadata(issuer_url: authorization_server)
|
|
55
55
|
ensure_issuer_matches!(expected: authorization_server, returned: as_metadata["issuer"])
|
|
56
56
|
ensure_secure_endpoints!(as_metadata)
|
|
57
|
+
|
|
58
|
+
if provider_authorization_flow == :client_credentials
|
|
59
|
+
return run_client_credentials!(as_metadata: as_metadata, prm: prm, resource: resource, scope: scope)
|
|
60
|
+
end
|
|
61
|
+
|
|
57
62
|
ensure_pkce_supported!(as_metadata)
|
|
58
63
|
|
|
59
64
|
client_info = ensure_client_registered(as_metadata: as_metadata)
|
|
@@ -92,14 +97,46 @@ module MCP
|
|
|
92
97
|
:authorized
|
|
93
98
|
end
|
|
94
99
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# token endpoint
|
|
98
|
-
#
|
|
100
|
+
# Runs the OAuth 2.1 `client_credentials` grant (machine-to-machine, no user interaction) and persists
|
|
101
|
+
# the resulting token. Shares the same discovery and security checks as `run!`; the only difference is
|
|
102
|
+
# the grant exchanged at the token endpoint. There is no PKCE, redirect, or authorization request,
|
|
103
|
+
# and no `offline_access` augmentation because the grant does not issue a refresh token (OAuth 2.1 Section 4.3.3).
|
|
104
|
+
# The pre-registered `client_id` / `client_secret` come from the provider's stored `client_information`.
|
|
105
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
|
|
106
|
+
def run_client_credentials!(as_metadata:, prm:, resource:, scope:)
|
|
107
|
+
client_info = client_credentials_client_info
|
|
108
|
+
|
|
109
|
+
form = { "grant_type" => "client_credentials" }
|
|
110
|
+
effective_scope = resolve_scope(scope: scope, prm: prm)
|
|
111
|
+
form["scope"] = effective_scope if effective_scope
|
|
112
|
+
form["resource"] = resource if resource
|
|
113
|
+
|
|
114
|
+
tokens = post_to_token_endpoint(as_metadata: as_metadata, client_info: client_info, form: form)
|
|
115
|
+
@provider.save_tokens(tokens)
|
|
116
|
+
:authorized
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Reads the pre-registered credentials for the `client_credentials` grant directly from the provider's stored
|
|
120
|
+
# `client_information`, rather than going through `ensure_client_registered` (which targets the authorization-code
|
|
121
|
+
# flow and reaches for `Provider`-only methods like `client_metadata` and `client_id_metadata_document_url`).
|
|
122
|
+
# The grant is for confidential clients, so a missing `client_id` is a clean configuration error, not a fallback
|
|
123
|
+
# to dynamic registration.
|
|
124
|
+
def client_credentials_client_info
|
|
125
|
+
info = @provider.client_information
|
|
126
|
+
unless info.is_a?(Hash) && client_info_required_value(info, "client_id")
|
|
127
|
+
raise AuthorizationError,
|
|
128
|
+
"Cannot run the client_credentials grant: the provider has no stored `client_id`."
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
info
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Exchanges the saved `refresh_token` for a fresh access token (RFC 6749 Section 6).
|
|
135
|
+
# Re-discovers PRM and AS metadata so we always pick up a moved token endpoint, and re-runs the audience / issuer / security
|
|
136
|
+
# checks before talking to it.
|
|
99
137
|
#
|
|
100
|
-
# Returns `:refreshed` on success. Raises `AuthorizationError` when
|
|
101
|
-
#
|
|
102
|
-
# the token endpoint refuses the refresh request.
|
|
138
|
+
# Returns `:refreshed` on success. Raises `AuthorizationError` when the provider has no refresh token, no client information,
|
|
139
|
+
# or when the token endpoint refuses the refresh request.
|
|
103
140
|
# https://www.rfc-editor.org/rfc/rfc6749#section-6
|
|
104
141
|
def refresh!(server_url:, resource_metadata_url: nil)
|
|
105
142
|
refresh_token = read_token("refresh_token")
|
|
@@ -110,8 +147,7 @@ module MCP
|
|
|
110
147
|
|
|
111
148
|
# A CIMD-configured provider stores no `client_information` on purpose
|
|
112
149
|
# (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.
|
|
150
|
+
# Allow refresh to proceed in that case so the `refresh_token` obtained via the CIMD flow remains usable.
|
|
115
151
|
have_cimd_url = !@provider.client_id_metadata_document_url.nil?
|
|
116
152
|
|
|
117
153
|
unless have_stored_client_info || have_cimd_url
|
|
@@ -484,6 +520,18 @@ module MCP
|
|
|
484
520
|
Array(grant_types).include?("refresh_token")
|
|
485
521
|
end
|
|
486
522
|
|
|
523
|
+
# The OAuth flow the provider drives. Dispatching on the provider's
|
|
524
|
+
# declared flow keeps `Flow` from second-guessing intent by parsing
|
|
525
|
+
# `client_metadata[:grant_types]` (which is protocol metadata for the
|
|
526
|
+
# authorization server, not an SDK control signal). A provider that
|
|
527
|
+
# predates this method is treated as the interactive authorization-code
|
|
528
|
+
# flow it was the only option for.
|
|
529
|
+
def provider_authorization_flow
|
|
530
|
+
return :authorization_code unless @provider.respond_to?(:authorization_flow)
|
|
531
|
+
|
|
532
|
+
@provider.authorization_flow
|
|
533
|
+
end
|
|
534
|
+
|
|
487
535
|
def build_authorization_url(as_metadata:, client_id:, scope:, state:, code_challenge:, resource:)
|
|
488
536
|
authorization_endpoint = as_metadata["authorization_endpoint"]
|
|
489
537
|
unless authorization_endpoint
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Client
|
|
5
5
|
module OAuth
|
|
6
|
-
# Pluggable OAuth client configuration
|
|
7
|
-
# the `oauth:` keyword.
|
|
8
|
-
#
|
|
6
|
+
# Pluggable OAuth client configuration for the OAuth 2.1 Authorization Code + PKCE flow,
|
|
7
|
+
# handed to `MCP::Client::HTTP` via the `oauth:` keyword.
|
|
8
|
+
# Inspired by the OAuthClientProvider in the TypeScript SDK and the httpx.Auth-based provider
|
|
9
|
+
# in the Python SDK. For the non-interactive machine-to-machine `client_credentials` grant,
|
|
10
|
+
# use `ClientCredentialsProvider` instead.
|
|
9
11
|
#
|
|
10
12
|
# Required keyword arguments:
|
|
11
13
|
# - `client_metadata` - Hash sent to the authorization server's Dynamic Client
|
|
@@ -36,6 +38,8 @@ module MCP
|
|
|
36
38
|
# DCR `client_metadata` MUST NOT include `client_id`, while the CIMD document MUST include `client_id` set
|
|
37
39
|
# to the URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
|
|
38
40
|
class Provider
|
|
41
|
+
include StorageBackedProvider
|
|
42
|
+
|
|
39
43
|
# Raised when `Provider#initialize` is called with a `redirect_uri` that
|
|
40
44
|
# is neither HTTPS nor a loopback `http://` URL, per the MCP
|
|
41
45
|
# authorization spec's Communication Security requirement.
|
|
@@ -102,28 +106,11 @@ module MCP
|
|
|
102
106
|
@client_id_metadata_document_url = client_id_metadata_document_url
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@storage.tokens
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def save_tokens(tokens)
|
|
114
|
-
@storage.save_tokens(tokens)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def client_information
|
|
118
|
-
@storage.client_information
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def save_client_information(info)
|
|
122
|
-
@storage.save_client_information(info)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def clear_tokens!
|
|
126
|
-
@storage.save_tokens(nil)
|
|
109
|
+
# Identifies the OAuth flow this provider drives.
|
|
110
|
+
# `Flow` dispatches on this rather than inspecting `client_metadata[:grant_types]`,
|
|
111
|
+
# which is protocol metadata for the authorization server, not an SDK control signal.
|
|
112
|
+
def authorization_flow
|
|
113
|
+
:authorization_code
|
|
127
114
|
end
|
|
128
115
|
end
|
|
129
116
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Client
|
|
5
|
+
module OAuth
|
|
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.
|
|
12
|
+
#
|
|
13
|
+
# Including classes must set `@storage` to an object responding to `tokens`,
|
|
14
|
+
# `save_tokens(tokens)`, `client_information`, and `save_client_information(info)`
|
|
15
|
+
# (see `InMemoryStorage`).
|
|
16
|
+
module StorageBackedProvider
|
|
17
|
+
def access_token
|
|
18
|
+
tokens&.dig("access_token") || tokens&.dig(:access_token)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def tokens
|
|
22
|
+
@storage.tokens
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save_tokens(tokens)
|
|
26
|
+
@storage.save_tokens(tokens)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def client_information
|
|
30
|
+
@storage.client_information
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def save_client_information(info)
|
|
34
|
+
@storage.save_client_information(info)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clear_tokens!
|
|
38
|
+
@storage.save_tokens(nil)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/mcp/client/oauth.rb
CHANGED
|
@@ -4,13 +4,15 @@ require_relative "oauth/discovery"
|
|
|
4
4
|
require_relative "oauth/flow"
|
|
5
5
|
require_relative "oauth/in_memory_storage"
|
|
6
6
|
require_relative "oauth/pkce"
|
|
7
|
+
require_relative "oauth/storage_backed_provider"
|
|
7
8
|
require_relative "oauth/provider"
|
|
9
|
+
require_relative "oauth/client_credentials_provider"
|
|
8
10
|
|
|
9
11
|
module MCP
|
|
10
12
|
class Client
|
|
11
13
|
# OAuth client support for the MCP Authorization spec (PRM discovery,
|
|
12
14
|
# Authorization Server metadata discovery, Dynamic Client Registration,
|
|
13
|
-
# OAuth 2.1 Authorization Code + PKCE).
|
|
15
|
+
# OAuth 2.1 Authorization Code + PKCE, and the client_credentials grant).
|
|
14
16
|
# https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
|
|
15
17
|
module OAuth
|
|
16
18
|
end
|
data/lib/mcp/client.rb
CHANGED
|
@@ -103,6 +103,8 @@ module MCP
|
|
|
103
103
|
# Returns a single page of tools from the server.
|
|
104
104
|
#
|
|
105
105
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
106
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
107
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
106
108
|
# @return [MCP::Client::ListToolsResult] Result with `tools` (Array<MCP::Client::Tool>)
|
|
107
109
|
# and `next_cursor` (String or nil).
|
|
108
110
|
#
|
|
@@ -114,9 +116,9 @@ module MCP
|
|
|
114
116
|
# cursor = page.next_cursor
|
|
115
117
|
# break unless cursor
|
|
116
118
|
# end
|
|
117
|
-
def list_tools(cursor: nil)
|
|
119
|
+
def list_tools(cursor: nil, meta: nil)
|
|
118
120
|
params = cursor ? { cursor: cursor } : nil
|
|
119
|
-
response = request(method: "tools/list", params: params)
|
|
121
|
+
response = request(method: "tools/list", params: params, meta: meta)
|
|
120
122
|
result = response["result"] || {}
|
|
121
123
|
|
|
122
124
|
tools = (result["tools"] || []).map do |tool|
|
|
@@ -152,11 +154,13 @@ module MCP
|
|
|
152
154
|
# Returns a single page of resources from the server.
|
|
153
155
|
#
|
|
154
156
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
157
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
158
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
155
159
|
# @return [MCP::Client::ListResourcesResult] Result with `resources` (Array<Hash>)
|
|
156
160
|
# and `next_cursor` (String or nil).
|
|
157
|
-
def list_resources(cursor: nil)
|
|
161
|
+
def list_resources(cursor: nil, meta: nil)
|
|
158
162
|
params = cursor ? { cursor: cursor } : nil
|
|
159
|
-
response = request(method: "resources/list", params: params)
|
|
163
|
+
response = request(method: "resources/list", params: params, meta: meta)
|
|
160
164
|
result = response["result"] || {}
|
|
161
165
|
|
|
162
166
|
ListResourcesResult.new(
|
|
@@ -181,11 +185,13 @@ module MCP
|
|
|
181
185
|
# Returns a single page of resource templates from the server.
|
|
182
186
|
#
|
|
183
187
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
188
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
189
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
184
190
|
# @return [MCP::Client::ListResourceTemplatesResult] Result with `resource_templates`
|
|
185
191
|
# (Array<Hash>) and `next_cursor` (String or nil).
|
|
186
|
-
def list_resource_templates(cursor: nil)
|
|
192
|
+
def list_resource_templates(cursor: nil, meta: nil)
|
|
187
193
|
params = cursor ? { cursor: cursor } : nil
|
|
188
|
-
response = request(method: "resources/templates/list", params: params)
|
|
194
|
+
response = request(method: "resources/templates/list", params: params, meta: meta)
|
|
189
195
|
result = response["result"] || {}
|
|
190
196
|
|
|
191
197
|
ListResourceTemplatesResult.new(
|
|
@@ -210,11 +216,13 @@ module MCP
|
|
|
210
216
|
# Returns a single page of prompts from the server.
|
|
211
217
|
#
|
|
212
218
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
219
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
220
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
213
221
|
# @return [MCP::Client::ListPromptsResult] Result with `prompts` (Array<Hash>)
|
|
214
222
|
# and `next_cursor` (String or nil).
|
|
215
|
-
def list_prompts(cursor: nil)
|
|
223
|
+
def list_prompts(cursor: nil, meta: nil)
|
|
216
224
|
params = cursor ? { cursor: cursor } : nil
|
|
217
|
-
response = request(method: "prompts/list", params: params)
|
|
225
|
+
response = request(method: "prompts/list", params: params, meta: meta)
|
|
218
226
|
result = response["result"] || {}
|
|
219
227
|
|
|
220
228
|
ListPromptsResult.new(
|
|
@@ -242,6 +250,10 @@ module MCP
|
|
|
242
250
|
# @param tool [MCP::Client::Tool] The tool to be called.
|
|
243
251
|
# @param arguments [Object, nil] The arguments to pass to the tool.
|
|
244
252
|
# @param progress_token [String, Integer, nil] A token to request progress notifications from the server during tool execution.
|
|
253
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
254
|
+
# e.g. the W3C Trace Context keys reserved by SEP-414
|
|
255
|
+
# (`MCP::TraceContext::TRACEPARENT_META_KEY`, `tracestate`, `baggage`).
|
|
256
|
+
# `progress_token` takes precedence over a `progressToken` entry in `meta`.
|
|
245
257
|
# @return [Hash] The full JSON-RPC response from the transport.
|
|
246
258
|
#
|
|
247
259
|
# @example Call by name
|
|
@@ -256,14 +268,17 @@ module MCP
|
|
|
256
268
|
# @note
|
|
257
269
|
# The exact requirements for `arguments` are determined by the transport layer in use.
|
|
258
270
|
# Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
|
|
259
|
-
def call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil)
|
|
271
|
+
def call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil, meta: nil)
|
|
260
272
|
tool_name = name || tool&.name
|
|
261
273
|
raise ArgumentError, "Either `name:` or `tool:` must be provided." unless tool_name
|
|
262
274
|
|
|
263
275
|
params = { name: tool_name, arguments: arguments }
|
|
276
|
+
meta_entries = meta ? meta.dup : {}
|
|
264
277
|
if progress_token
|
|
265
|
-
|
|
278
|
+
meta_entries.delete("progressToken")
|
|
279
|
+
meta_entries[:progressToken] = progress_token
|
|
266
280
|
end
|
|
281
|
+
params[:_meta] = meta_entries unless meta_entries.empty?
|
|
267
282
|
|
|
268
283
|
request(method: "tools/call", params: params)
|
|
269
284
|
end
|
|
@@ -271,9 +286,11 @@ module MCP
|
|
|
271
286
|
# Reads a resource from the server by URI and returns the contents.
|
|
272
287
|
#
|
|
273
288
|
# @param uri [String] The URI of the resource to read.
|
|
289
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
290
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
274
291
|
# @return [Array<Hash>] An array of resource contents (text or blob).
|
|
275
|
-
def read_resource(uri:)
|
|
276
|
-
response = request(method: "resources/read", params: { uri: uri })
|
|
292
|
+
def read_resource(uri:, meta: nil)
|
|
293
|
+
response = request(method: "resources/read", params: { uri: uri }, meta: meta)
|
|
277
294
|
|
|
278
295
|
response.dig("result", "contents") || []
|
|
279
296
|
end
|
|
@@ -281,9 +298,11 @@ module MCP
|
|
|
281
298
|
# Gets a prompt from the server by name and returns its details.
|
|
282
299
|
#
|
|
283
300
|
# @param name [String] The name of the prompt to get.
|
|
301
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
302
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
284
303
|
# @return [Hash] A hash containing the prompt details.
|
|
285
|
-
def get_prompt(name:)
|
|
286
|
-
response = request(method: "prompts/get", params: { name: name })
|
|
304
|
+
def get_prompt(name:, meta: nil)
|
|
305
|
+
response = request(method: "prompts/get", params: { name: name }, meta: meta)
|
|
287
306
|
|
|
288
307
|
response.fetch("result", {})
|
|
289
308
|
end
|
|
@@ -294,12 +313,14 @@ module MCP
|
|
|
294
313
|
# or `{ type: "ref/resource", uri: "file:///{path}" }`.
|
|
295
314
|
# @param argument [Hash] The argument being completed, e.g. `{ name: "language", value: "py" }`.
|
|
296
315
|
# @param context [Hash, nil] Optional context with previously resolved arguments.
|
|
316
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
317
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
297
318
|
# @return [Hash] The completion result with `"values"`, `"hasMore"`, and optionally `"total"`.
|
|
298
|
-
def complete(ref:, argument:, context: nil)
|
|
319
|
+
def complete(ref:, argument:, context: nil, meta: nil)
|
|
299
320
|
params = { ref: ref, argument: argument }
|
|
300
321
|
params[:context] = context if context
|
|
301
322
|
|
|
302
|
-
response = request(method: "completion/complete", params: params)
|
|
323
|
+
response = request(method: "completion/complete", params: params, meta: meta)
|
|
303
324
|
|
|
304
325
|
response.dig("result", "completion") || { "values" => [], "hasMore" => false }
|
|
305
326
|
end
|
|
@@ -315,8 +336,8 @@ module MCP
|
|
|
315
336
|
# client.ping # => {}
|
|
316
337
|
#
|
|
317
338
|
# @see https://modelcontextprotocol.io/specification/latest/basic/utilities/ping
|
|
318
|
-
def ping
|
|
319
|
-
result = request(method: Methods::PING)["result"]
|
|
339
|
+
def ping(meta: nil)
|
|
340
|
+
result = request(method: Methods::PING, meta: meta)["result"]
|
|
320
341
|
raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash)
|
|
321
342
|
|
|
322
343
|
result
|
|
@@ -345,7 +366,13 @@ module MCP
|
|
|
345
366
|
pages
|
|
346
367
|
end
|
|
347
368
|
|
|
348
|
-
|
|
369
|
+
# Merges caller-supplied `meta` entries into the request params as `_meta`,
|
|
370
|
+
# without mutating the caller's hashes. Per SEP-414, `_meta` carries
|
|
371
|
+
# request-specific metadata such as W3C trace context (`traceparent`,
|
|
372
|
+
# `tracestate`, `baggage`); see {MCP::TraceContext}.
|
|
373
|
+
def request(method:, params: nil, meta: nil)
|
|
374
|
+
params = (params || {}).merge(_meta: meta) if meta && !meta.empty?
|
|
375
|
+
|
|
349
376
|
request_body = {
|
|
350
377
|
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
351
378
|
id: request_id,
|
data/lib/mcp/configuration.rb
CHANGED
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, :size, :meta
|
|
8
|
+
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :annotations, :size, :meta
|
|
9
9
|
|
|
10
|
-
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, size: nil, meta: nil)
|
|
10
|
+
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, annotations: 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
|
+
@annotations = annotations
|
|
17
18
|
@size = size
|
|
18
19
|
@meta = meta
|
|
19
20
|
end
|
|
@@ -26,6 +27,7 @@ module MCP
|
|
|
26
27
|
description: description,
|
|
27
28
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
28
29
|
mimeType: mime_type,
|
|
30
|
+
annotations: annotations&.to_h,
|
|
29
31
|
size: size,
|
|
30
32
|
_meta: meta,
|
|
31
33
|
}.compact
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class ResourceTemplate
|
|
5
|
-
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta
|
|
5
|
+
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :annotations, :meta
|
|
6
6
|
|
|
7
|
-
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
7
|
+
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, annotations: nil, meta: nil)
|
|
8
8
|
@uri_template = uri_template
|
|
9
9
|
@name = name
|
|
10
10
|
@title = title
|
|
11
11
|
@description = description
|
|
12
12
|
@icons = icons
|
|
13
13
|
@mime_type = mime_type
|
|
14
|
+
@annotations = annotations
|
|
14
15
|
@meta = meta
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -22,6 +23,7 @@ module MCP
|
|
|
22
23
|
description: description,
|
|
23
24
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
24
25
|
mimeType: mime_type,
|
|
26
|
+
annotations: annotations&.to_h,
|
|
25
27
|
_meta: meta,
|
|
26
28
|
}.compact
|
|
27
29
|
end
|
|
@@ -389,7 +389,11 @@ module MCP
|
|
|
389
389
|
end
|
|
390
390
|
rescue StandardError => e
|
|
391
391
|
MCP.configuration.exception_reporter.call(e, { request: body_string })
|
|
392
|
-
|
|
392
|
+
json_rpc_error_response(
|
|
393
|
+
status: 500,
|
|
394
|
+
code: JsonRpcHandler::ErrorCode::INTERNAL_ERROR,
|
|
395
|
+
message: "Internal server error",
|
|
396
|
+
)
|
|
393
397
|
end
|
|
394
398
|
|
|
395
399
|
def handle_get(request)
|
|
@@ -513,19 +517,19 @@ module MCP
|
|
|
513
517
|
media_type = content_type&.split(";")&.first&.strip&.downcase
|
|
514
518
|
return if media_type == "application/json"
|
|
515
519
|
|
|
516
|
-
|
|
517
|
-
415,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
520
|
+
json_rpc_error_response(
|
|
521
|
+
status: 415,
|
|
522
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
523
|
+
message: "Unsupported Media Type: Content-Type must be application/json",
|
|
524
|
+
)
|
|
521
525
|
end
|
|
522
526
|
|
|
523
527
|
def not_acceptable_response(required_types)
|
|
524
|
-
|
|
525
|
-
406,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
528
|
+
json_rpc_error_response(
|
|
529
|
+
status: 406,
|
|
530
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
531
|
+
message: "Not Acceptable: Accept header must include #{required_types.join(" and ")}",
|
|
532
|
+
)
|
|
529
533
|
end
|
|
530
534
|
|
|
531
535
|
def parse_request_body(body_string)
|
|
@@ -535,7 +539,11 @@ module MCP
|
|
|
535
539
|
end
|
|
536
540
|
|
|
537
541
|
def invalid_json_response
|
|
538
|
-
|
|
542
|
+
json_rpc_error_response(
|
|
543
|
+
status: 400,
|
|
544
|
+
code: JsonRpcHandler::ErrorCode::PARSE_ERROR,
|
|
545
|
+
message: "Parse error: Invalid JSON",
|
|
546
|
+
)
|
|
539
547
|
end
|
|
540
548
|
|
|
541
549
|
def initialize_request?(body)
|
|
@@ -543,20 +551,20 @@ module MCP
|
|
|
543
551
|
end
|
|
544
552
|
|
|
545
553
|
def validate_protocol_version_header(request)
|
|
546
|
-
header_value = request.env["HTTP_MCP_PROTOCOL_VERSION"]
|
|
547
|
-
return if header_value.nil?
|
|
554
|
+
header_value = request.env["HTTP_MCP_PROTOCOL_VERSION"] || MCP::Configuration::DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
|
548
555
|
return if MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(header_value)
|
|
549
556
|
|
|
550
557
|
supported = MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.join(", ")
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
558
|
+
json_rpc_error_response(
|
|
559
|
+
status: 400,
|
|
560
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
561
|
+
message: "Bad Request: Unsupported protocol version: #{header_value}. Supported versions: #{supported}",
|
|
562
|
+
)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def json_rpc_error_response(status:, code:, message:)
|
|
566
|
+
body = { jsonrpc: "2.0", id: nil, error: { code: code, message: message } }
|
|
567
|
+
[status, { "Content-Type" => "application/json" }, [body.to_json]]
|
|
560
568
|
end
|
|
561
569
|
|
|
562
570
|
def notification?(body)
|
|
@@ -793,15 +801,27 @@ module MCP
|
|
|
793
801
|
end
|
|
794
802
|
|
|
795
803
|
def method_not_allowed_response
|
|
796
|
-
|
|
804
|
+
json_rpc_error_response(
|
|
805
|
+
status: 405,
|
|
806
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
807
|
+
message: "Method not allowed",
|
|
808
|
+
)
|
|
797
809
|
end
|
|
798
810
|
|
|
799
811
|
def missing_session_id_response
|
|
800
|
-
|
|
812
|
+
json_rpc_error_response(
|
|
813
|
+
status: 400,
|
|
814
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
815
|
+
message: "Missing session ID",
|
|
816
|
+
)
|
|
801
817
|
end
|
|
802
818
|
|
|
803
819
|
def session_not_found_response
|
|
804
|
-
|
|
820
|
+
json_rpc_error_response(
|
|
821
|
+
status: 404,
|
|
822
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
823
|
+
message: "Session not found",
|
|
824
|
+
)
|
|
805
825
|
end
|
|
806
826
|
|
|
807
827
|
def already_initialized_response(request_id)
|
|
@@ -821,11 +841,11 @@ module MCP
|
|
|
821
841
|
end
|
|
822
842
|
|
|
823
843
|
def session_already_connected_response
|
|
824
|
-
|
|
825
|
-
409,
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
844
|
+
json_rpc_error_response(
|
|
845
|
+
status: 409,
|
|
846
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
847
|
+
message: "Conflict: Only one SSE stream is allowed per session",
|
|
848
|
+
)
|
|
829
849
|
end
|
|
830
850
|
|
|
831
851
|
def setup_sse_stream(session_id)
|
data/lib/mcp/server.rb
CHANGED
|
@@ -58,6 +58,32 @@ module MCP
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Raised when a requested resource URI does not exist. Per SEP-2164,
|
|
62
|
+
# resource-not-found errors use the standard JSON-RPC Invalid Params code (-32602)
|
|
63
|
+
# with the requested URI in the error `data` member. Raise this from
|
|
64
|
+
# a `resources_read_handler` block for unknown URIs:
|
|
65
|
+
#
|
|
66
|
+
# server.resources_read_handler do |params|
|
|
67
|
+
# raise MCP::Server::ResourceNotFoundError.new(params[:uri], params) unless known?(params[:uri])
|
|
68
|
+
# do_something(params[:uri])
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164
|
|
72
|
+
class ResourceNotFoundError < RequestHandlerError
|
|
73
|
+
def initialize(uri, request = nil)
|
|
74
|
+
# The explicit `error_code` keeps the descriptive message in the JSON-RPC
|
|
75
|
+
# error response; `error_type: :invalid_params` alone would replace it
|
|
76
|
+
# with the generic "Invalid params" string.
|
|
77
|
+
super(
|
|
78
|
+
"Resource not found: #{uri}",
|
|
79
|
+
request,
|
|
80
|
+
error_type: :invalid_params,
|
|
81
|
+
error_code: JsonRpcHandler::ErrorCode::INVALID_PARAMS,
|
|
82
|
+
error_data: { uri: uri },
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
61
87
|
class MethodAlreadyDefinedError < StandardError
|
|
62
88
|
attr_reader :method_name
|
|
63
89
|
|
|
@@ -846,7 +872,7 @@ module MCP
|
|
|
846
872
|
uri = ref[:uri]
|
|
847
873
|
found = @resource_index.key?(uri) || @resource_templates.any? { |t| t.uri_template == uri }
|
|
848
874
|
unless found
|
|
849
|
-
raise
|
|
875
|
+
raise ResourceNotFoundError.new(uri, params)
|
|
850
876
|
end
|
|
851
877
|
else
|
|
852
878
|
raise RequestHandlerError.new("Invalid ref type: #{ref[:type]}", params, error_type: :invalid_params)
|
|
@@ -12,9 +12,9 @@ module MCP
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def missing_required_arguments(arguments)
|
|
15
|
-
return [] unless schema[:required].is_a?(Array)
|
|
15
|
+
return [] unless @schema[:required].is_a?(Array)
|
|
16
16
|
|
|
17
|
-
(schema[:required] - arguments.keys.map(&:to_s))
|
|
17
|
+
(@schema[:required] - arguments.keys.map(&:to_s))
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def validate_arguments(arguments)
|
data/lib/mcp/tool/schema.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
|
-
require "
|
|
4
|
+
require "json_schemer"
|
|
5
5
|
|
|
6
6
|
module MCP
|
|
7
7
|
class Tool
|
|
@@ -38,11 +38,10 @@ module MCP
|
|
|
38
38
|
|
|
39
39
|
# JSON Schema 2020-12 is the default dialect for MCP schema definitions
|
|
40
40
|
# per MCP 2025-11-25 (SEP-1613). Note: emission only — runtime validation
|
|
41
|
-
# is still performed against the JSON Schema draft-04 metaschema
|
|
42
|
-
# the `json-schema` gem does not yet support 2020-12.
|
|
41
|
+
# is still performed against the JSON Schema draft-04 metaschema.
|
|
43
42
|
JSON_SCHEMA_2020_12_URI = "https://json-schema.org/draft/2020-12/schema"
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
DRAFT4_META_SCHEMA_URI = "http://json-schema.org/draft-04/schema#"
|
|
46
45
|
|
|
47
46
|
def initialize(schema = {})
|
|
48
47
|
@schema = JSON.parse(JSON.dump(schema), symbolize_names: true)
|
|
@@ -51,7 +50,7 @@ module MCP
|
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
def ==(other)
|
|
54
|
-
other.is_a?(self.class) && schema == other.schema
|
|
53
|
+
other.is_a?(self.class) && @schema == other.instance_variable_get(:@schema)
|
|
55
54
|
end
|
|
56
55
|
|
|
57
56
|
def to_h
|
|
@@ -62,8 +61,38 @@ module MCP
|
|
|
62
61
|
|
|
63
62
|
private
|
|
64
63
|
|
|
64
|
+
def stringify(obj)
|
|
65
|
+
case obj
|
|
66
|
+
when Hash
|
|
67
|
+
obj.each_with_object({}) { |(k, v), h| h[k.to_s] = stringify(v) }
|
|
68
|
+
when Array
|
|
69
|
+
obj.map { |v| stringify(v) }
|
|
70
|
+
when Symbol
|
|
71
|
+
obj.to_s
|
|
72
|
+
else
|
|
73
|
+
obj
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Lazily built so a cache hit in `validate_schema!` avoids the schemer construction cost.
|
|
78
|
+
# Memoized per Schema instance because schema content is fixed at construction,
|
|
79
|
+
# so the compiled schemer is reusable across many `fully_validate` calls.
|
|
80
|
+
#
|
|
81
|
+
# `format: false` preserves the legacy behavior of the previous `json-schema` based implementation,
|
|
82
|
+
# which did not enforce `format` keywords. `RegexpError` from a malformed `pattern` is re-raised as
|
|
83
|
+
# `ArgumentError` so callers see the same exception class they used to.
|
|
84
|
+
def schemer
|
|
85
|
+
@schemer ||= JSONSchemer.schema(
|
|
86
|
+
stringify(schema_for_validation),
|
|
87
|
+
meta_schema: DRAFT4_META_SCHEMA_URI,
|
|
88
|
+
format: false,
|
|
89
|
+
)
|
|
90
|
+
rescue RegexpError => e
|
|
91
|
+
raise ArgumentError, "Invalid JSON Schema: #{e.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
65
94
|
def fully_validate(data)
|
|
66
|
-
|
|
95
|
+
schemer.validate(stringify(data)).map { |validation_error| validation_error.fetch("error") }
|
|
67
96
|
end
|
|
68
97
|
|
|
69
98
|
def validate_schema!
|
|
@@ -75,16 +104,7 @@ module MCP
|
|
|
75
104
|
key = Digest::SHA256.hexdigest(JSON.generate(target, max_nesting: false))
|
|
76
105
|
return if VALIDATION_CACHE.validated?(key)
|
|
77
106
|
|
|
78
|
-
|
|
79
|
-
schema_reader = JSON::Schema::Reader.new(
|
|
80
|
-
accept_uri: false,
|
|
81
|
-
accept_file: ->(path) { File.realpath(path.to_s).start_with?(gem_path) },
|
|
82
|
-
)
|
|
83
|
-
metaschema_path = Pathname.new(JSON::Validator.validator_for_name("draft4").metaschema)
|
|
84
|
-
# Converts metaschema to a file URI for cross-platform compatibility
|
|
85
|
-
metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
|
|
86
|
-
metaschema = metaschema_uri.to_s
|
|
87
|
-
errors = JSON::Validator.fully_validate(metaschema, target, schema_reader: schema_reader)
|
|
107
|
+
errors = schemer.validate_schema.map { |validation_error| validation_error.fetch("error") }
|
|
88
108
|
if errors.any?
|
|
89
109
|
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
|
90
110
|
end
|
|
@@ -92,9 +112,8 @@ module MCP
|
|
|
92
112
|
VALIDATION_CACHE.store(key)
|
|
93
113
|
end
|
|
94
114
|
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# (whether SDK-injected by `to_h` or user-supplied) does not break the validator.
|
|
115
|
+
# `json_schemer` is pinned to the draft-04 metaschema, so strip top-level `$schema` before validation:
|
|
116
|
+
# this preserves the legacy behavior of ignoring the advertised dialect URI when the SDK validates schemas.
|
|
98
117
|
def schema_for_validation
|
|
99
118
|
return @schema unless @schema.key?(:"$schema")
|
|
100
119
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
# Reserved `_meta` keys for W3C Trace Context propagation, per SEP-414.
|
|
5
|
+
#
|
|
6
|
+
# The MCP spec reserves the un-prefixed `_meta` keys `traceparent`, `tracestate`, and `baggage`
|
|
7
|
+
# (an explicit exception to the reverse-DNS prefix rule for `_meta` keys) so that clients and
|
|
8
|
+
# servers can propagate distributed-tracing context across MCP requests.
|
|
9
|
+
# The SDK guarantees these keys pass through incoming request `_meta` untouched; tool, prompt,
|
|
10
|
+
# and resource handlers can read them from `server_context[:_meta]` and bridge them to a tracing
|
|
11
|
+
# system such as the `opentelemetry-ruby` gems. The SDK itself does not depend on OpenTelemetry.
|
|
12
|
+
#
|
|
13
|
+
# - https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414
|
|
14
|
+
# - https://www.w3.org/TR/trace-context/
|
|
15
|
+
# - https://www.w3.org/TR/baggage/
|
|
16
|
+
module TraceContext
|
|
17
|
+
TRACEPARENT_META_KEY = "traceparent"
|
|
18
|
+
TRACESTATE_META_KEY = "tracestate"
|
|
19
|
+
BAGGAGE_META_KEY = "baggage"
|
|
20
|
+
|
|
21
|
+
META_KEYS = [TRACEPARENT_META_KEY, TRACESTATE_META_KEY, BAGGAGE_META_KEY].freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/mcp/version.rb
CHANGED
data/lib/mcp.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.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Model Context Protocol
|
|
@@ -10,19 +10,19 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: json_schemer
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '4
|
|
18
|
+
version: '2.4'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '4
|
|
25
|
+
version: '2.4'
|
|
26
26
|
description: The official Ruby SDK for Model Context Protocol servers and clients
|
|
27
27
|
email:
|
|
28
28
|
- mcp-support@anthropic.com
|
|
@@ -42,11 +42,13 @@ files:
|
|
|
42
42
|
- lib/mcp/client.rb
|
|
43
43
|
- lib/mcp/client/http.rb
|
|
44
44
|
- lib/mcp/client/oauth.rb
|
|
45
|
+
- lib/mcp/client/oauth/client_credentials_provider.rb
|
|
45
46
|
- lib/mcp/client/oauth/discovery.rb
|
|
46
47
|
- lib/mcp/client/oauth/flow.rb
|
|
47
48
|
- lib/mcp/client/oauth/in_memory_storage.rb
|
|
48
49
|
- lib/mcp/client/oauth/pkce.rb
|
|
49
50
|
- lib/mcp/client/oauth/provider.rb
|
|
51
|
+
- lib/mcp/client/oauth/storage_backed_provider.rb
|
|
50
52
|
- lib/mcp/client/paginated_result.rb
|
|
51
53
|
- lib/mcp/client/stdio.rb
|
|
52
54
|
- lib/mcp/client/tool.rb
|
|
@@ -80,6 +82,7 @@ files:
|
|
|
80
82
|
- lib/mcp/tool/output_schema.rb
|
|
81
83
|
- lib/mcp/tool/response.rb
|
|
82
84
|
- lib/mcp/tool/schema.rb
|
|
85
|
+
- lib/mcp/trace_context.rb
|
|
83
86
|
- lib/mcp/transport.rb
|
|
84
87
|
- lib/mcp/version.rb
|
|
85
88
|
homepage: https://ruby.sdk.modelcontextprotocol.io
|
|
@@ -87,7 +90,7 @@ licenses:
|
|
|
87
90
|
- Apache-2.0
|
|
88
91
|
metadata:
|
|
89
92
|
allowed_push_host: https://rubygems.org
|
|
90
|
-
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.
|
|
93
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.20.0
|
|
91
94
|
homepage_uri: https://ruby.sdk.modelcontextprotocol.io
|
|
92
95
|
source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
93
96
|
bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
|