mcp 1.0.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 +428 -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/tool.rb +3 -2
- data/lib/mcp/client.rb +364 -41
- data/lib/mcp/configuration.rb +84 -7
- data/lib/mcp/elicitation/enum_schema.rb +121 -0
- data/lib/mcp/elicitation.rb +10 -0
- data/lib/mcp/error_codes.rb +14 -8
- 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 +117 -0
- 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 +541 -43
- data/lib/mcp/server_context.rb +92 -5
- data/lib/mcp/server_session.rb +77 -15
- data/lib/mcp/tool/response.rb +8 -2
- data/lib/mcp/transport.rb +7 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +3 -0
- metadata +11 -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
|
|
@@ -872,6 +930,53 @@ class WeatherTool < MCP::Tool
|
|
|
872
930
|
end
|
|
873
931
|
```
|
|
874
932
|
|
|
933
|
+
### Tool Responses with Image, Audio, and Embedded Resources
|
|
934
|
+
|
|
935
|
+
Tool responses are not limited to text. The `MCP::Content` module provides `Image`, `Audio`, and `EmbeddedResource` content types,
|
|
936
|
+
which serialize to the `image`, `audio`, and `resource` content blocks defined by the MCP spec. Image and audio data is passed as
|
|
937
|
+
a base64-encoded string together with its MIME type:
|
|
938
|
+
|
|
939
|
+
```ruby
|
|
940
|
+
class ChartTool < MCP::Tool
|
|
941
|
+
description "Render a chart as a PNG image"
|
|
942
|
+
|
|
943
|
+
def self.call(server_context:)
|
|
944
|
+
MCP::Tool::Response.new([
|
|
945
|
+
MCP::Content::Text.new("Here is the rendered chart:").to_h,
|
|
946
|
+
MCP::Content::Image.new(Base64.strict_encode64(render_chart_png), "image/png").to_h,
|
|
947
|
+
])
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
class SpeechTool < MCP::Tool
|
|
952
|
+
description "Synthesize speech audio"
|
|
953
|
+
|
|
954
|
+
def self.call(server_context:)
|
|
955
|
+
MCP::Tool::Response.new([
|
|
956
|
+
MCP::Content::Audio.new(Base64.strict_encode64(synthesize_wav), "audio/wav").to_h,
|
|
957
|
+
])
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
An embedded resource wraps `MCP::Resource::TextContents` or `MCP::Resource::BlobContents`, allowing a tool to return resource contents inline:
|
|
963
|
+
|
|
964
|
+
```ruby
|
|
965
|
+
class ReportTool < MCP::Tool
|
|
966
|
+
description "Return a report as an embedded resource"
|
|
967
|
+
|
|
968
|
+
def self.call(server_context:)
|
|
969
|
+
contents = MCP::Resource::TextContents.new(
|
|
970
|
+
uri: "report://monthly",
|
|
971
|
+
mime_type: "application/json",
|
|
972
|
+
text: { total: 42 }.to_json,
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
MCP::Tool::Response.new([MCP::Content::EmbeddedResource.new(contents).to_h])
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
```
|
|
979
|
+
|
|
875
980
|
### Prompts
|
|
876
981
|
|
|
877
982
|
MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
|
|
@@ -1010,6 +1115,49 @@ The server will handle prompt listing and execution through the MCP protocol met
|
|
|
1010
1115
|
- `prompts/list` - Lists all registered prompts and their schemas
|
|
1011
1116
|
- `prompts/get` - Retrieves and executes a specific prompt with arguments
|
|
1012
1117
|
|
|
1118
|
+
### Prompts with Image and Embedded Resource Content
|
|
1119
|
+
|
|
1120
|
+
Prompt messages are not limited to text. The same `MCP::Content` types used in tool responses can be used as message content,
|
|
1121
|
+
letting a prompt template include images or inline resource contents. Unlike tool responses, the content object is passed directly rather than as a hash;
|
|
1122
|
+
`MCP::Prompt::Message` serializes it when the prompt result is returned:
|
|
1123
|
+
|
|
1124
|
+
```ruby
|
|
1125
|
+
class CodeReviewPrompt < MCP::Prompt
|
|
1126
|
+
prompt_name "code_review"
|
|
1127
|
+
description "Review a source file with an accompanying diagram"
|
|
1128
|
+
arguments [
|
|
1129
|
+
MCP::Prompt::Argument.new(name: "file_uri", description: "URI of the file to review", required: true),
|
|
1130
|
+
]
|
|
1131
|
+
|
|
1132
|
+
class << self
|
|
1133
|
+
def template(args, server_context:)
|
|
1134
|
+
MCP::Prompt::Result.new(
|
|
1135
|
+
messages: [
|
|
1136
|
+
MCP::Prompt::Message.new(
|
|
1137
|
+
role: "user",
|
|
1138
|
+
content: MCP::Content::EmbeddedResource.new(
|
|
1139
|
+
MCP::Resource::TextContents.new(
|
|
1140
|
+
uri: args["file_uri"],
|
|
1141
|
+
mime_type: "text/x-ruby",
|
|
1142
|
+
text: read_source(args["file_uri"]),
|
|
1143
|
+
),
|
|
1144
|
+
),
|
|
1145
|
+
),
|
|
1146
|
+
MCP::Prompt::Message.new(
|
|
1147
|
+
role: "user",
|
|
1148
|
+
content: MCP::Content::Image.new(architecture_diagram_base64, "image/png"),
|
|
1149
|
+
),
|
|
1150
|
+
MCP::Prompt::Message.new(
|
|
1151
|
+
role: "user",
|
|
1152
|
+
content: MCP::Content::Text.new("Please review the code above, using the diagram for context."),
|
|
1153
|
+
),
|
|
1154
|
+
],
|
|
1155
|
+
)
|
|
1156
|
+
end
|
|
1157
|
+
end
|
|
1158
|
+
end
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1013
1161
|
### Resources
|
|
1014
1162
|
|
|
1015
1163
|
MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
|
|
@@ -1126,6 +1274,34 @@ server.resources_read_handler do |params|
|
|
|
1126
1274
|
end
|
|
1127
1275
|
```
|
|
1128
1276
|
|
|
1277
|
+
### Reading Binary Resources
|
|
1278
|
+
|
|
1279
|
+
For binary resources, respond with a base64-encoded `blob` field instead of `text`.
|
|
1280
|
+
The `MCP::Resource::TextContents` and `MCP::Resource::BlobContents` classes build the two contents shapes defined by the spec:
|
|
1281
|
+
|
|
1282
|
+
```ruby
|
|
1283
|
+
server.resources_read_handler do |params|
|
|
1284
|
+
case params[:uri]
|
|
1285
|
+
when "file:///logo.png"
|
|
1286
|
+
[
|
|
1287
|
+
MCP::Resource::BlobContents.new(
|
|
1288
|
+
uri: params[:uri],
|
|
1289
|
+
mime_type: "image/png",
|
|
1290
|
+
data: Base64.strict_encode64(File.binread("logo.png")),
|
|
1291
|
+
).to_h,
|
|
1292
|
+
]
|
|
1293
|
+
else
|
|
1294
|
+
[
|
|
1295
|
+
MCP::Resource::TextContents.new(
|
|
1296
|
+
uri: params[:uri],
|
|
1297
|
+
mime_type: "text/plain",
|
|
1298
|
+
text: "Hello from example resource!",
|
|
1299
|
+
).to_h,
|
|
1300
|
+
]
|
|
1301
|
+
end
|
|
1302
|
+
end
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1129
1305
|
### Resource Templates
|
|
1130
1306
|
|
|
1131
1307
|
Resource templates follow the same pattern. Class-based templates declare a `uri_template` and
|
|
@@ -1201,6 +1377,31 @@ server = MCP::Server.new(
|
|
|
1201
1377
|
)
|
|
1202
1378
|
```
|
|
1203
1379
|
|
|
1380
|
+
Registered templates are listed through the `resources/templates/list` protocol method.
|
|
1381
|
+
To serve reads for URIs that match a template, extract the variable parts of the URI in your `resources_read_handler`:
|
|
1382
|
+
|
|
1383
|
+
```ruby
|
|
1384
|
+
resource_template = MCP::ResourceTemplate.new(
|
|
1385
|
+
uri_template: "file:///items/{item_id}",
|
|
1386
|
+
name: "item",
|
|
1387
|
+
mime_type: "application/json",
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
server = MCP::Server.new(name: "my_server", resource_templates: [resource_template])
|
|
1391
|
+
|
|
1392
|
+
server.resources_read_handler do |params|
|
|
1393
|
+
if (match = params[:uri].match(%r{\Afile:///items/(?<item_id>[^/]+)\z}))
|
|
1394
|
+
[{
|
|
1395
|
+
uri: params[:uri],
|
|
1396
|
+
mimeType: "application/json",
|
|
1397
|
+
text: { id: match[:item_id] }.to_json,
|
|
1398
|
+
}]
|
|
1399
|
+
else
|
|
1400
|
+
raise MCP::Server::ResourceNotFoundError.new(params[:uri], params)
|
|
1401
|
+
end
|
|
1402
|
+
end
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1204
1405
|
### Roots
|
|
1205
1406
|
|
|
1206
1407
|
The Model Context Protocol allows servers to request filesystem roots from clients through the `roots/list` method.
|
|
@@ -1218,6 +1419,22 @@ Roots define the boundaries of where a server can operate, providing a list of d
|
|
|
1218
1419
|
> automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding
|
|
1219
1420
|
> `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning.
|
|
1220
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
|
+
|
|
1221
1438
|
**Using Roots in Tools:**
|
|
1222
1439
|
|
|
1223
1440
|
Tools that accept a `server_context:` parameter can call `list_roots` on it.
|
|
@@ -1793,6 +2010,98 @@ server.define_tool(name: "collect_contact", description: "Collect contact info")
|
|
|
1793
2010
|
end
|
|
1794
2011
|
```
|
|
1795
2012
|
|
|
2013
|
+
The `requested_schema` must be a flat object schema: a top-level `type: "object"` whose `properties` are limited to
|
|
2014
|
+
primitive types (`string`, `number`, `integer`, `boolean`). Nested objects and arrays are not allowed, which keeps
|
|
2015
|
+
the schema simple enough for clients to render as a form. Per the MCP specification, the client validates
|
|
2016
|
+
the user's input against this schema before returning it, so the `content` of an `accept` response matches the requested shape.
|
|
2017
|
+
|
|
2018
|
+
#### Default Values and Enums
|
|
2019
|
+
|
|
2020
|
+
Properties may declare a `default` value (SEP-1034), which clients use to pre-fill the form.
|
|
2021
|
+
String properties may declare `enum` values, optionally with human-readable `enumNames` (SEP-1330), which clients render as a choice list:
|
|
2022
|
+
|
|
2023
|
+
```ruby
|
|
2024
|
+
server.define_tool(name: "configure_deploy", description: "Configure a deployment") do |server_context:|
|
|
2025
|
+
result = server_context.create_form_elicitation(
|
|
2026
|
+
message: "Configure the deployment",
|
|
2027
|
+
requested_schema: {
|
|
2028
|
+
type: "object",
|
|
2029
|
+
properties: {
|
|
2030
|
+
replicas: { type: "integer", default: 3 },
|
|
2031
|
+
verbose: { type: "boolean", default: false },
|
|
2032
|
+
environment: {
|
|
2033
|
+
type: "string",
|
|
2034
|
+
enum: ["dev", "staging", "prod"],
|
|
2035
|
+
enumNames: ["Development", "Staging", "Production"],
|
|
2036
|
+
default: "dev",
|
|
2037
|
+
},
|
|
2038
|
+
},
|
|
2039
|
+
required: ["environment"],
|
|
2040
|
+
},
|
|
2041
|
+
)
|
|
2042
|
+
|
|
2043
|
+
MCP::Tool::Response.new([{ type: "text", text: "Deploying to #{result[:content][:environment]}" }])
|
|
2044
|
+
end
|
|
2045
|
+
```
|
|
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
|
+
|
|
1796
2105
|
#### URL Mode
|
|
1797
2106
|
|
|
1798
2107
|
URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
|
|
@@ -1921,6 +2230,18 @@ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
|
1921
2230
|
# When tools change, notify clients
|
|
1922
2231
|
server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
|
|
1923
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
|
|
1924
2245
|
```
|
|
1925
2246
|
|
|
1926
2247
|
You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
|
|
@@ -2081,13 +2402,18 @@ If your application needs the complete collection regardless of how the server i
|
|
|
2081
2402
|
`client.tools`, `client.resources`, `client.resource_templates`, and `client.prompts` auto-iterate
|
|
2082
2403
|
through all pages and return a plain array of items, guaranteeing the full collection regardless
|
|
2083
2404
|
of the server's `page_size` setting. When a server paginates, they issue multiple JSON-RPC round
|
|
2084
|
-
trips per call
|
|
2085
|
-
|
|
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.
|
|
2086
2407
|
|
|
2087
2408
|
```ruby
|
|
2088
2409
|
tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
|
|
2089
2410
|
```
|
|
2090
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
|
+
|
|
2091
2417
|
Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
|
|
2092
2418
|
fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
|
|
2093
2419
|
|
|
@@ -2097,8 +2423,11 @@ Per SEP-2549, list and read results can carry cache hints telling clients how lo
|
|
|
2097
2423
|
`0` means do not cache) and whether shared intermediaries may cache it (`cacheScope`: `"public"` or `"private"`).
|
|
2098
2424
|
|
|
2099
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`,
|
|
2100
|
-
`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).
|
|
2101
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.
|
|
2102
2431
|
|
|
2103
2432
|
```ruby
|
|
2104
2433
|
server = MCP::Server.new(
|
|
@@ -2199,6 +2528,55 @@ This class supports:
|
|
|
2199
2528
|
Clients are initialized with a transport layer instance that handles the low-level communication mechanics.
|
|
2200
2529
|
Authorization is handled by the transport layer.
|
|
2201
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
|
+
|
|
2202
2580
|
## Transport Layer Interface
|
|
2203
2581
|
|
|
2204
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:
|
|
@@ -2322,6 +2700,13 @@ The server will send `notifications/progress` back to the client during executio
|
|
|
2322
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
|
|
2323
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.
|
|
2324
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
|
+
|
|
2325
2710
|
#### Server-to-Client Requests (Elicitation)
|
|
2326
2711
|
|
|
2327
2712
|
Servers can send requests back to the client while one of the client's own requests is in flight - for example,
|
|
@@ -2595,6 +2980,28 @@ whether the swap was triggered by
|
|
|
2595
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,
|
|
2596
2981
|
and whether the new URL is `http://` *or* `https://` to a different host or tenant.
|
|
2597
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
|
+
|
|
2598
3005
|
#### Customizing the Faraday Connection
|
|
2599
3006
|
|
|
2600
3007
|
You can pass a block to `MCP::Client::HTTP.new` to customize the underlying Faraday connection.
|
|
@@ -2625,6 +3032,18 @@ The Ruby client recognizes such results and raises `MCP::Client::InputRequiredEr
|
|
|
2625
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`
|
|
2626
3033
|
are provided for forward compatibility. Servers on stable protocol versions never send `resultType`, so existing behavior is unchanged.
|
|
2627
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
|
+
|
|
2628
3047
|
## Conformance Testing
|
|
2629
3048
|
|
|
2630
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
|