mcp 1.1.0 → 1.2.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 +251 -9
- data/lib/json_rpc_handler.rb +3 -2
- data/lib/mcp/client/http.rb +338 -63
- data/lib/mcp/client/mcp_param_headers.rb +242 -0
- data/lib/mcp/client/modern_envelope.rb +32 -0
- data/lib/mcp/client/oauth/discovery.rb +108 -14
- data/lib/mcp/client/oauth/flow.rb +95 -7
- data/lib/mcp/client/stdio.rb +243 -66
- data/lib/mcp/client.rb +363 -41
- data/lib/mcp/configuration.rb +72 -10
- data/lib/mcp/elicitation/enum_schema.rb +121 -0
- data/lib/mcp/elicitation.rb +10 -0
- data/lib/mcp/instrumentation.rb +6 -0
- data/lib/mcp/methods.rb +21 -0
- data/lib/mcp/prompt.rb +8 -1
- data/lib/mcp/protocol_deprecations.rb +61 -0
- data/lib/mcp/request_envelope.rb +39 -17
- data/lib/mcp/server/input_required_result.rb +163 -0
- data/lib/mcp/server/pending_response.rb +62 -0
- data/lib/mcp/server/request_state_security.rb +131 -0
- data/lib/mcp/server/transports/stdio_transport.rb +39 -2
- data/lib/mcp/server/transports/streamable_http_transport.rb +710 -20
- data/lib/mcp/server.rb +492 -37
- data/lib/mcp/server_context.rb +92 -5
- data/lib/mcp/server_session.rb +77 -15
- data/lib/mcp/transport.rb +7 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69ef263cadd9221259fedfea19655d64cdecd7175deca3139439577b48fb10b2
|
|
4
|
+
data.tar.gz: 4580aafd0866efce3f102a701d244dd67af7b8881b19306d4b81ab2701a7350a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5aae701572da5725edf8e02ba8da55debfc702183b123813aba22ca005bf3ee1e61193543975c111065b49e5eddac520600fc775a026fc04d5b28050d22313b
|
|
7
|
+
data.tar.gz: bd8a9bce3c67b2f4db31bd0bb58b85e9417c997be075f07a9e597a2819facc922c8cea9796fe9ed543b498ec9e3d080fa24b9ce0246e660d5dc875c94b995c0e
|
data/README.md
CHANGED
|
@@ -46,8 +46,38 @@ It implements the Model Context Protocol specification, handling model context r
|
|
|
46
46
|
### Supported Methods
|
|
47
47
|
|
|
48
48
|
- `initialize` - Initializes the protocol and returns server capabilities
|
|
49
|
-
- `server/discover` - Sessionless capability discovery (MCP 2026-07-28
|
|
50
|
-
|
|
49
|
+
- `server/discover` - Sessionless capability discovery (MCP 2026-07-28, SEP-2575): returns the modern `supportedVersions`,
|
|
50
|
+
`capabilities`, `instructions`, the required `ttlMs`/`cacheScope` cache hints, and the server identity as the optional
|
|
51
|
+
`io.modelcontextprotocol/serverInfo` stamp in the result `_meta`, and responds before `initialize`
|
|
52
|
+
and without an `Mcp-Session-Id`. The server also serves the full stateless modern lifecycle: requests carrying the SEP-2575 `_meta` envelope
|
|
53
|
+
(`io.modelcontextprotocol/protocolVersion`, `clientInfo`, and `clientCapabilities`) are validated per request,
|
|
54
|
+
and the Streamable HTTP transport serves them on a sessionless single-exchange path. On the client, `MCP::Client#connect` negotiates
|
|
55
|
+
the lifecycle automatically by default (probe `server/discover`, fall back to the `initialize` handshake), `connect(mode: :modern)` skips
|
|
56
|
+
the handshake entirely, `connect(mode: :legacy)` forces the classic handshake, and `MCP::Client#discover` exposes the raw discovery result
|
|
57
|
+
- `subscriptions/listen` - Long-lived notification subscription stream (MCP 2026-07-28, SEP-2575), replacing the legacy HTTP GET listening stream:
|
|
58
|
+
the client opts in via the `notifications` filter (`toolsListChanged` / `promptsListChanged` / `resourcesListChanged` / `resourceSubscriptions`),
|
|
59
|
+
the server acknowledges the honored subset with `notifications/subscriptions/acknowledged` as the first stream message,
|
|
60
|
+
and every delivered notification carries the correlating `io.modelcontextprotocol/subscriptionId` in `_meta`. Served on the Streamable HTTP modern path;
|
|
61
|
+
stdio answers `-32601`. Concurrent streams are capped by `max_listen_subscriptions:` (default 1000), and each stream receives an SSE keepalive
|
|
62
|
+
comment frame every `listen_keepalive_interval:` seconds (default 15) so a dropped connection frees its slot; pass `listen_keepalive_interval: nil`
|
|
63
|
+
when an upstream proxy already keeps the stream alive
|
|
64
|
+
- Multi round-trip `input_required` results (MCP 2026-07-28, SEP-2322): a `tools/call`, `prompts/get`, or `resources/read` handler that
|
|
65
|
+
opts in to `server_context:` may return `MCP::Server::InputRequiredResult.new(input_requests:, request_state:)` to ask the client for
|
|
66
|
+
additional input (`elicitation/create`, `sampling/createMessage`, or `roots/list` shapes) instead of performing a server-initiated request,
|
|
67
|
+
which the modern lifecycle forbids. On the retried request the handler re-runs from the start and reads the answers via
|
|
68
|
+
`server_context.input_responses` / `server_context.input_response(key)` and the echoed opaque `server_context.request_state`
|
|
69
|
+
(deterministic replay; the server holds no memory between rounds). The SDK rejects issuance on legacy requests and returns `-32021`
|
|
70
|
+
when an embedded request needs a client capability the request did not declare. The echoed `requestState` arrives as
|
|
71
|
+
client-controlled input: pass `MCP::Server::RequestStateSecurity.new(key:)` (a 32-byte key) via `Server.new(request_state_security:)` to
|
|
72
|
+
have it sealed with AES-256-GCM and bound to a TTL plus the originating method, target, and arguments, all transparently to handlers.
|
|
73
|
+
Multi-process deployments must share the key across workers; without `request_state_security:` the state crosses the wire exactly as
|
|
74
|
+
the handler wrote it and protecting it is the handler author's responsibility. On the client, register handlers with
|
|
75
|
+
`on_elicitation` / `on_sampling` / `on_roots` - the same registrations that answer a real server-to-client request - and
|
|
76
|
+
declare the matching capabilities on `connect` (a server embeds only the request kinds the client declared);
|
|
77
|
+
`call_tool` / `get_prompt` / `read_resource` then resume `input_required` results automatically: each embedded request is fulfilled by
|
|
78
|
+
the matching handler and the original request is re-issued with `inputResponses` plus the echoed `requestState`
|
|
79
|
+
(with exponential backoff for `requestState`-only load-shedding legs). Without a matching handler they raise `MCP::Client::InputRequiredError`,
|
|
80
|
+
and the `input_responses:` / `request_state:` keyword arguments support manual driving
|
|
51
81
|
- `ping` - Simple health check
|
|
52
82
|
- `logging/setLevel` - Configures the minimum log level for the server
|
|
53
83
|
- `tools/list` - Lists all registered tools and their schemas
|
|
@@ -346,7 +376,8 @@ server = MCP::Server.new(
|
|
|
346
376
|
```
|
|
347
377
|
|
|
348
378
|
This hash is then passed as the `server_context` keyword argument to tool and prompt calls.
|
|
349
|
-
Note that exception
|
|
379
|
+
Note that the exception reporter does not receive this user-defined hash, and instrumentation
|
|
380
|
+
callbacks omit it unless you opt in with `instrument_server_context`.
|
|
350
381
|
See the relevant sections below for the arguments they receive.
|
|
351
382
|
|
|
352
383
|
#### Request-specific `_meta` Parameter
|
|
@@ -460,10 +491,31 @@ around_request = ->(data, &request_handler) { request_handler.call }
|
|
|
460
491
|
|
|
461
492
|
**`data` availability by timing:**
|
|
462
493
|
|
|
463
|
-
- Before `request_handler.call`: `method`
|
|
494
|
+
- Before `request_handler.call`: `method`, and `server_context` when `instrument_server_context` is enabled
|
|
464
495
|
- After `request_handler.call`: `tool_name`, `tool_arguments`, `prompt_name`, `resource_uri`, `error`, `client`
|
|
465
496
|
- Not available inside `around_request`: `duration` (added after `around_request` returns)
|
|
466
497
|
|
|
498
|
+
**Exposing the user-defined `server_context` (opt in):**
|
|
499
|
+
|
|
500
|
+
`data` omits the user-defined `server_context` by default, because that hash is
|
|
501
|
+
application-supplied and may hold values a tracing backend should not receive.
|
|
502
|
+
Enable it when you need to tag spans with the request's subject:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
MCP.configure do |config|
|
|
506
|
+
config.instrument_server_context = true
|
|
507
|
+
|
|
508
|
+
config.around_request = ->(data, &request_handler) {
|
|
509
|
+
Sentry.set_user(id: data.dig(:server_context, :user_id))
|
|
510
|
+
request_handler.call
|
|
511
|
+
}
|
|
512
|
+
end
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`data[:server_context]` is the hash passed to `Server.new` — `nil` when the host
|
|
516
|
+
set none. It is not the exception reporter's context argument, which describes
|
|
517
|
+
where a failure occurred rather than who made the request.
|
|
518
|
+
|
|
467
519
|
> [!NOTE]
|
|
468
520
|
> `tool_name`, `prompt_name` and `resource_uri` may only be populated for the corresponding request methods
|
|
469
521
|
> (`tools/call`, `prompts/get`, `resources/read`), and may not be set depending on how the request is handled
|
|
@@ -515,6 +567,8 @@ It receives a hash with the following possible keys:
|
|
|
515
567
|
- `error`: (String, optional) Error code if a lookup failed
|
|
516
568
|
- `duration`: (Float) Duration of the call in seconds
|
|
517
569
|
- `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
|
|
570
|
+
- `server_context`: (Any, optional) The user-defined hash passed to `Server.new`, present only when
|
|
571
|
+
`instrument_server_context` is enabled
|
|
518
572
|
|
|
519
573
|
**Signature:**
|
|
520
574
|
|
|
@@ -531,8 +585,7 @@ configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
|
|
|
531
585
|
MCP::Server.new(name: "test_server", configuration: configuration)
|
|
532
586
|
```
|
|
533
587
|
|
|
534
|
-
If no protocol version is specified, the latest
|
|
535
|
-
The latest stable version includes new features from the [draft version](https://modelcontextprotocol.io/specification/draft).
|
|
588
|
+
If no protocol version is specified, the latest handshake version (`2025-11-25`) is applied by default.
|
|
536
589
|
|
|
537
590
|
This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:
|
|
538
591
|
|
|
@@ -542,6 +595,11 @@ MCP::Configuration.new(protocol_version: nil)
|
|
|
542
595
|
|
|
543
596
|
If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
|
|
544
597
|
|
|
598
|
+
The pin scopes the `initialize` handshake, so it accepts handshake versions (`2025-11-25` and earlier) only. Per the SEP-2575 era model,
|
|
599
|
+
`2026-07-28` carries its version on every request and has no handshake at all, so there is nothing for a pin to configure there and setting it raises `ArgumentError`;
|
|
600
|
+
a client asking `initialize` for a modern version is counter-offered the pinned version (or the latest handshake version), matching the TypeScript and Python SDKs.
|
|
601
|
+
Clients reach `2026-07-28` through `server/discover` and the per-request `_meta` envelope, which the bundled transports serve alongside the handshake with no configuration needed.
|
|
602
|
+
|
|
545
603
|
Be sure to check the [MCP spec](https://modelcontextprotocol.io/specification/versioning) for the protocol version to understand the supported features for the version being set.
|
|
546
604
|
|
|
547
605
|
### Exception Reporting
|
|
@@ -1361,6 +1419,22 @@ Roots define the boundaries of where a server can operate, providing a list of d
|
|
|
1361
1419
|
> automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding
|
|
1362
1420
|
> `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning.
|
|
1363
1421
|
|
|
1422
|
+
**Timeouts:** every server-to-client request is bounded, so a client that never answers cannot park the handler's thread indefinitely.
|
|
1423
|
+
`MCP::Server::Transports::StreamableHTTPTransport` waits `server_to_client_request_timeout:` seconds (600 by default), then tells
|
|
1424
|
+
the client the request was abandoned and raises `MCP::Server::RequestTimeoutError`. Individual calls override the deadline with `timeout:`,
|
|
1425
|
+
which is the knob to reach for when a prompt legitimately waits on a person:
|
|
1426
|
+
|
|
1427
|
+
```ruby
|
|
1428
|
+
server_context.create_form_elicitation(
|
|
1429
|
+
message: "Approve this deployment?",
|
|
1430
|
+
requested_schema: { type: "object", properties: { approved: { type: "boolean" } } },
|
|
1431
|
+
timeout: 3600, # This one waits up to an hour.
|
|
1432
|
+
)
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
`StdioTransport` is not bounded and ignores `timeout:`: it owns the client process, so a client that stops answering
|
|
1436
|
+
surfaces as end-of-file rather than as a wait that never ends.
|
|
1437
|
+
|
|
1364
1438
|
**Using Roots in Tools:**
|
|
1365
1439
|
|
|
1366
1440
|
Tools that accept a `server_context:` parameter can call `list_roots` on it.
|
|
@@ -1970,6 +2044,64 @@ server.define_tool(name: "configure_deploy", description: "Configure a deploymen
|
|
|
1970
2044
|
end
|
|
1971
2045
|
```
|
|
1972
2046
|
|
|
2047
|
+
#### Enum Schemas
|
|
2048
|
+
|
|
2049
|
+
For enumerated choices, use `MCP::Elicitation::EnumSchema` to construct the canonical schema shapes per
|
|
2050
|
+
[SEP-1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330) instead of building
|
|
2051
|
+
the underlying Hash by hand. The five class methods cover titled and untitled, single-select and multi-select,
|
|
2052
|
+
plus the legacy `enumNames` form retained for backward compatibility:
|
|
2053
|
+
|
|
2054
|
+
```ruby
|
|
2055
|
+
size_schema = MCP::Elicitation::EnumSchema.titled_single_select(
|
|
2056
|
+
options: [
|
|
2057
|
+
{ value: "s", title: "Small" },
|
|
2058
|
+
{ value: "m", title: "Medium" },
|
|
2059
|
+
{ value: "l", title: "Large" },
|
|
2060
|
+
],
|
|
2061
|
+
default: "m",
|
|
2062
|
+
)
|
|
2063
|
+
|
|
2064
|
+
tags_schema = MCP::Elicitation::EnumSchema.untitled_multi_select(
|
|
2065
|
+
values: ["urgent", "billing", "feedback"],
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
result = server_context.create_form_elicitation(
|
|
2069
|
+
message: "Tell us about your order",
|
|
2070
|
+
requested_schema: {
|
|
2071
|
+
type: "object",
|
|
2072
|
+
properties: {
|
|
2073
|
+
size: size_schema.to_h,
|
|
2074
|
+
tags: tags_schema.to_h,
|
|
2075
|
+
},
|
|
2076
|
+
required: ["size"],
|
|
2077
|
+
},
|
|
2078
|
+
)
|
|
2079
|
+
```
|
|
2080
|
+
|
|
2081
|
+
The available builders are `untitled_single_select`, `titled_single_select`, `untitled_multi_select`, `titled_multi_select`,
|
|
2082
|
+
and `legacy_titled`. Each accepts optional `default:`, `title:`, and `description:`.
|
|
2083
|
+
|
|
2084
|
+
The same builders produce the `requestedSchema` of an `elicitation/create` request embedded in a SEP-2322 `input_required` result,
|
|
2085
|
+
which is how elicitation reaches clients on the stateless 2026-07-28 lifecycle:
|
|
2086
|
+
|
|
2087
|
+
```ruby
|
|
2088
|
+
MCP::Server::InputRequiredResult.new(
|
|
2089
|
+
input_requests: {
|
|
2090
|
+
"size" => {
|
|
2091
|
+
method: "elicitation/create",
|
|
2092
|
+
params: {
|
|
2093
|
+
message: "Pick a size",
|
|
2094
|
+
requestedSchema: {
|
|
2095
|
+
type: "object",
|
|
2096
|
+
properties: { size: size_schema.to_h },
|
|
2097
|
+
required: ["size"],
|
|
2098
|
+
},
|
|
2099
|
+
},
|
|
2100
|
+
},
|
|
2101
|
+
},
|
|
2102
|
+
)
|
|
2103
|
+
```
|
|
2104
|
+
|
|
1973
2105
|
#### URL Mode
|
|
1974
2106
|
|
|
1975
2107
|
URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
|
|
@@ -2098,6 +2230,18 @@ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
|
2098
2230
|
# When tools change, notify clients
|
|
2099
2231
|
server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
|
|
2100
2232
|
server.notify_tools_list_changed
|
|
2233
|
+
|
|
2234
|
+
# When prompts change, notify clients
|
|
2235
|
+
server.define_prompt(name: "new_prompt") do |args, server_context:|
|
|
2236
|
+
MCP::Prompt::Result.new(messages: [])
|
|
2237
|
+
end
|
|
2238
|
+
server.notify_prompts_list_changed
|
|
2239
|
+
|
|
2240
|
+
# When resources change, notify clients
|
|
2241
|
+
server.define_resource(uri: "resource://new", name: "new_resource", mime_type: "text/plain") do
|
|
2242
|
+
[MCP::Resource::TextContents.new(uri: "resource://new", mime_type: "text/plain", text: "contents")]
|
|
2243
|
+
end
|
|
2244
|
+
server.notify_resources_list_changed
|
|
2101
2245
|
```
|
|
2102
2246
|
|
|
2103
2247
|
You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
|
|
@@ -2258,13 +2402,18 @@ If your application needs the complete collection regardless of how the server i
|
|
|
2258
2402
|
`client.tools`, `client.resources`, `client.resource_templates`, and `client.prompts` auto-iterate
|
|
2259
2403
|
through all pages and return a plain array of items, guaranteeing the full collection regardless
|
|
2260
2404
|
of the server's `page_size` setting. When a server paginates, they issue multiple JSON-RPC round
|
|
2261
|
-
trips per call
|
|
2262
|
-
|
|
2405
|
+
trips per call. Two guards keep that loop finite: it stops when the server returns a `nextCursor`
|
|
2406
|
+
it has already sent, and it stops after `max_pages` pages.
|
|
2263
2407
|
|
|
2264
2408
|
```ruby
|
|
2265
2409
|
tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
|
|
2266
2410
|
```
|
|
2267
2411
|
|
|
2412
|
+
`MCP::Client.new` accepts an optional `max_pages:` keyword that caps how many pages these methods
|
|
2413
|
+
will walk. It defaults to `1_000`; a server that keeps offering a fresh `nextCursor` past that
|
|
2414
|
+
point raises `MCP::Client::PaginationLimitError` rather than being followed indefinitely. Raise it
|
|
2415
|
+
if you legitimately expect more pages than that.
|
|
2416
|
+
|
|
2268
2417
|
Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
|
|
2269
2418
|
fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
|
|
2270
2419
|
|
|
@@ -2274,8 +2423,11 @@ Per SEP-2549, list and read results can carry cache hints telling clients how lo
|
|
|
2274
2423
|
`0` means do not cache) and whether shared intermediaries may cache it (`cacheScope`: `"public"` or `"private"`).
|
|
2275
2424
|
|
|
2276
2425
|
Emission is opt-in: pass `ttl_ms:` and/or `cache_scope:` to `MCP::Server.new` and both fields are added to `tools/list`, `prompts/list`, `resources/list`,
|
|
2277
|
-
`resources/templates/list`, and `resources/read` results (a missing field is filled with the defaults `ttlMs: 0` / `cacheScope: "
|
|
2426
|
+
`resources/templates/list`, and `resources/read` results (a missing field is filled with the defaults `ttlMs: 0` / `cacheScope: "private"`,
|
|
2427
|
+
the scope that keeps a potentially user-dependent result out of shared caches).
|
|
2278
2428
|
When neither is set, responses are serialized exactly as before.
|
|
2429
|
+
The 2026-07-28 revision makes both hints required on these results, so on requests carrying the modern `_meta` envelope
|
|
2430
|
+
the server always emits them, filling unset values with the same defaults; stable protocol versions keep the opt-in behavior.
|
|
2279
2431
|
|
|
2280
2432
|
```ruby
|
|
2281
2433
|
server = MCP::Server.new(
|
|
@@ -2376,6 +2528,55 @@ This class supports:
|
|
|
2376
2528
|
Clients are initialized with a transport layer instance that handles the low-level communication mechanics.
|
|
2377
2529
|
Authorization is handled by the transport layer.
|
|
2378
2530
|
|
|
2531
|
+
### Lifecycle Negotiation (SEP-2575)
|
|
2532
|
+
|
|
2533
|
+
`MCP::Client#connect` selects the protocol lifecycle automatically by default: on the bundled
|
|
2534
|
+
`MCP::Client::HTTP` and `MCP::Client::Stdio` transports it probes `server/discover` first and adopts
|
|
2535
|
+
the stateless modern lifecycle (MCP 2026-07-28) when the server serves it, falling back to
|
|
2536
|
+
the classic `initialize` handshake otherwise. Custom transports whose `connect` does not declare
|
|
2537
|
+
a `mode:` keyword always receive the classic call shape, unchanged.
|
|
2538
|
+
|
|
2539
|
+
```ruby
|
|
2540
|
+
client.connect # negotiate automatically (default)
|
|
2541
|
+
client.connect(mode: :legacy) # force the classic initialize handshake
|
|
2542
|
+
client.connect(mode: :modern) # require the modern lifecycle; fails on legacy-only servers
|
|
2543
|
+
client.connect(protocol_version: "2025-11-25") # an explicit legacy version pins the handshake, no probe
|
|
2544
|
+
```
|
|
2545
|
+
|
|
2546
|
+
Prefer `mode: :legacy` for spawn-per-invocation CLI tools (the probe adds a round trip per process)
|
|
2547
|
+
and when using server-initiated requests (`on_elicitation` / `on_sampling`), which exist only on
|
|
2548
|
+
the legacy lifecycle.
|
|
2549
|
+
|
|
2550
|
+
Because the raw `connect` return value and `MCP::Client#server_info` mirror the wire result,
|
|
2551
|
+
their shape depends on the negotiated lifecycle: `InitializeResult` (`protocolVersion`,
|
|
2552
|
+
top-level `serverInfo`) on legacy, `DiscoverResult` (`supportedVersions`, `ttlMs`/`cacheScope`)
|
|
2553
|
+
on modern. Code that should work against both lifecycles can use the era-independent readers instead:
|
|
2554
|
+
|
|
2555
|
+
```ruby
|
|
2556
|
+
client.protocol_version # negotiated or adopted version, either lifecycle
|
|
2557
|
+
client.server_capabilities # capabilities Hash, either lifecycle
|
|
2558
|
+
client.instructions # instructions text, either lifecycle
|
|
2559
|
+
client.server_implementation # server name/version; nil when a modern server does not identify itself
|
|
2560
|
+
```
|
|
2561
|
+
|
|
2562
|
+
Troubleshooting: if `server_info["protocolVersion"]` starts returning `nil` after a server you connect to was upgraded,
|
|
2563
|
+
the server now serves the modern lifecycle and the automatic negotiation adopted it.
|
|
2564
|
+
Pass `mode: :legacy` for an immediate return to the previous behavior, or switch to the readers above for a permanent fix.
|
|
2565
|
+
|
|
2566
|
+
### Custom Headers from Tool Parameters (SEP-2243)
|
|
2567
|
+
|
|
2568
|
+
On a modern `MCP::Client::HTTP` connection, `tools/call` mirrors arguments whose `inputSchema` property carries
|
|
2569
|
+
an `x-mcp-header` annotation into `Mcp-Param-{Name}` request headers, so intermediaries can route
|
|
2570
|
+
on the values without parsing bodies. The declarations are learned from `tools/list` responses:
|
|
2571
|
+
list the tools before calling one to enable the mirroring. Values that cannot ride as plain ASCII header values
|
|
2572
|
+
(non-ASCII, control characters, edge whitespace, empty strings) are wrapped as `=?base64?...?=`,
|
|
2573
|
+
and a `null` or absent argument omits its header.
|
|
2574
|
+
|
|
2575
|
+
Per the specification, a tool definition whose `x-mcp-header` annotations are invalid (empty or non-token names,
|
|
2576
|
+
duplicate names, non-primitive properties, annotations outside a chain of `properties` keys) is excluded from
|
|
2577
|
+
`tools/list` results on modern connections, with a warning naming the tool.
|
|
2578
|
+
Legacy connections are unaffected: nothing is learned, mirrored, or excluded.
|
|
2579
|
+
|
|
2379
2580
|
## Transport Layer Interface
|
|
2380
2581
|
|
|
2381
2582
|
If the transport layer you need is not included in the gem, you can build and pass your own instances so long as they conform to the following interface:
|
|
@@ -2499,6 +2700,13 @@ The server will send `notifications/progress` back to the client during executio
|
|
|
2499
2700
|
an SSE event or a JSON response body. A message that reaches this limit before completing is rejected as a transport error, preventing unbounded memory growth from
|
|
2500
2701
|
a server that never terminates an SSE event. It defaults to `4 * 1024 * 1024` (4 MiB); raise it if your server returns larger responses.
|
|
2501
2702
|
|
|
2703
|
+
`MCP::Client::HTTP.new` also accepts `max_reconnection_wait:`, a budget in seconds for resuming a closed SSE stream. It gates every wait between reconnection attempts,
|
|
2704
|
+
and what is left of it becomes the read timeout of each resumed stream. The server chooses that wait through the SSE `retry:` field, and resuming happens on the calling thread,
|
|
2705
|
+
so without a budget a server answering with a large `retry:` parks a thread of your application for as long as it likes. It defaults to `300` (5 minutes).
|
|
2706
|
+
The server's `retry:` is never shortened: when honoring it would run past the budget, the client stops trying to resume and raises instead,
|
|
2707
|
+
the same thing it already does once the reconnection attempts are used up. A floor of 100ms applies to each wait, so a `retry: 0` cannot spin
|
|
2708
|
+
the listening stream's reconnect loop; waiting longer than the server asked for is explicitly allowed by the SSE reconnection algorithm the spec points at.
|
|
2709
|
+
|
|
2502
2710
|
#### Server-to-Client Requests (Elicitation)
|
|
2503
2711
|
|
|
2504
2712
|
Servers can send requests back to the client while one of the client's own requests is in flight - for example,
|
|
@@ -2772,6 +2980,28 @@ whether the swap was triggered by
|
|
|
2772
2980
|
`instance_variable_set(:@url, ...)`, by a Faraday customizer rewriting `url_prefix`, or by a custom middleware rewriting `env.url` (including just `env.url.query`) at request time,
|
|
2773
2981
|
and whether the new URL is `http://` *or* `https://` to a different host or tenant.
|
|
2774
2982
|
|
|
2983
|
+
##### Discovery URL Destinations
|
|
2984
|
+
|
|
2985
|
+
The scheme rules above say how a URL is contacted, not where it points. Discovery URLs arrive from the network, so the SDK also constrains their destinations.
|
|
2986
|
+
Both checks run before the request is sent, and neither is configurable.
|
|
2987
|
+
|
|
2988
|
+
- The `resource_metadata` URL in a `WWW-Authenticate` challenge must be on the MCP server's own origin. Protected Resource Metadata describes that server,
|
|
2989
|
+
so a real deployment publishes it there; requiring it means a `401` cannot aim the first request of the flow at an unrelated host. This is stricter than RFC 9728,
|
|
2990
|
+
which does not require it.
|
|
2991
|
+
- The PRM `authorization_servers` entry and the `authorization_endpoint`, `token_endpoint`, and `registration_endpoint` from Authorization Server metadata must not be
|
|
2992
|
+
IP literals in a private, loopback, link-local, or unique-local range, per the SSRF precaution in [RFC 9728 Section 7.7](https://www.rfc-editor.org/rfc/rfc9728#section-7.7).
|
|
2993
|
+
The blocked ranges are `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`, `::/96`, `fc00::/7`,
|
|
2994
|
+
and `fe80::/10`, along with the IPv4-mapped IPv6 spellings of each and the `localhost` name.
|
|
2995
|
+
- That range check is skipped when the MCP server URL you configured is itself on such an address. Pointing the client at a private network is a deliberate act,
|
|
2996
|
+
and the authorization server for it usually lives on the same network, so `http://localhost` development and deployments that never leave a corporate network keep working.
|
|
2997
|
+
|
|
2998
|
+
The range check compares IP literals and does not resolve hostnames, so it cannot recognize an internal service that is named rather than addressed,
|
|
2999
|
+
such as `https://vault.corp.internal/`. Resolving names here would not close that gap either, because the address the SDK looked up need not be the one
|
|
3000
|
+
the HTTP client connects to a moment later. The same-origin rule is what protects the `resource_metadata` URL, which is the only one of these a server supplies directly.
|
|
3001
|
+
|
|
3002
|
+
If you replace the OAuth HTTP client through `MCP::Client::OAuth::Flow.new(http_client_factory:)`, do not add redirect-following middleware. Every check above runs against
|
|
3003
|
+
the URL as written, so a connection that follows a `3xx` on its own would reach hosts these rules just refused.
|
|
3004
|
+
|
|
2775
3005
|
#### Customizing the Faraday Connection
|
|
2776
3006
|
|
|
2777
3007
|
You can pass a block to `MCP::Client::HTTP.new` to customize the underlying Faraday connection.
|
|
@@ -2802,6 +3032,18 @@ The Ruby client recognizes such results and raises `MCP::Client::InputRequiredEr
|
|
|
2802
3032
|
and the raw `result`; automatic resumption is not implemented yet, so callers respond manually if they opt into the draft flow. `MCP::ResultType::COMPLETE` and `MCP::ResultType::INPUT_REQUIRED`
|
|
2803
3033
|
are provided for forward compatibility. Servers on stable protocol versions never send `resultType`, so existing behavior is unchanged.
|
|
2804
3034
|
|
|
3035
|
+
SEP-2322 also makes `resultType` a required member of every result a 2026-07-28 server returns. The server stamps `resultType: "complete"` on all results of requests carrying
|
|
3036
|
+
the modern `_meta` envelope (and on `server/discover` results), while results that already carry a discriminator (`"input_required"`, the tasks extension's `"task"`) keep it.
|
|
3037
|
+
Legacy results stay unstamped, and clients treat an absent `resultType` as `"complete"` per the spec.
|
|
3038
|
+
|
|
3039
|
+
#### Dual-era authoring (legacy fulfilment shim)
|
|
3040
|
+
|
|
3041
|
+
Handlers written in the 2026 style serve pre-2026 clients too: when a `tools/call`, `prompts/get`, or `resources/read` handler returns an `InputRequiredResult` on the legacy wire,
|
|
3042
|
+
the server fulfills it in place of the client's driver. Each `inputRequests` entry is sent as the equivalent real server-to-client request
|
|
3043
|
+
(`elicitation/create`, `sampling/createMessage`, `roots/list`), associated with the originating request per SEP-2260; the answers are collected under the same keys,
|
|
3044
|
+
and the handler re-runs with `server_context.input_responses` populated and the raw `requestState` echoed, the same deterministic replay contract the modern client driver follows.
|
|
3045
|
+
The shim is on by default (matching the TypeScript SDK) and capped at 8 rounds; `MCP::Server.new(input_required_legacy_shim: false)` restores the strict rejection of `input_required` results on legacy requests.
|
|
3046
|
+
|
|
2805
3047
|
## Conformance Testing
|
|
2806
3048
|
|
|
2807
3049
|
The `conformance/` directory contains a test server and runner that validate the SDK against the MCP specification using [`@modelcontextprotocol/conformance`](https://github.com/modelcontextprotocol/conformance).
|
data/lib/json_rpc_handler.rb
CHANGED
|
@@ -135,11 +135,12 @@ module JsonRpcHandler
|
|
|
135
135
|
success_response(id: id, result: result)
|
|
136
136
|
rescue MCP::Server::RequestHandlerError => e
|
|
137
137
|
handle_request_error(e, id, id_validation_pattern)
|
|
138
|
-
rescue StandardError
|
|
138
|
+
rescue StandardError
|
|
139
|
+
# The exception message is deliberately not echoed into `data`: it can carry
|
|
140
|
+
# internals that must not reach untrusted clients (CWE-209).
|
|
139
141
|
error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
|
|
140
142
|
code: ErrorCode::INTERNAL_ERROR,
|
|
141
143
|
message: "Internal error",
|
|
142
|
-
data: e.message,
|
|
143
144
|
})
|
|
144
145
|
end
|
|
145
146
|
end
|