mcp 0.22.0 → 0.24.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 +324 -10
- data/lib/json_rpc_handler.rb +17 -0
- data/lib/mcp/apps.rb +109 -0
- data/lib/mcp/client/elicitation.rb +51 -0
- data/lib/mcp/client/http.rb +326 -31
- data/lib/mcp/client/oauth/client_credentials_provider.rb +72 -14
- data/lib/mcp/client/oauth/flow.rb +148 -5
- data/lib/mcp/client/oauth/in_memory_storage.rb +3 -1
- data/lib/mcp/client/oauth/jwt_client_assertion.rb +128 -0
- data/lib/mcp/client/oauth/provider.rb +11 -3
- data/lib/mcp/client/oauth.rb +1 -0
- data/lib/mcp/client/paginated_result.rb +7 -5
- data/lib/mcp/client/stdio.rb +47 -11
- data/lib/mcp/client.rb +89 -1
- data/lib/mcp/error_codes.rb +21 -0
- data/lib/mcp/methods.rb +7 -1
- data/lib/mcp/resource.rb +135 -0
- data/lib/mcp/resource_template.rb +149 -0
- data/lib/mcp/result_type.rb +18 -0
- data/lib/mcp/server/transports/stdio_transport.rb +51 -3
- data/lib/mcp/server/transports/streamable_http_transport.rb +385 -94
- data/lib/mcp/server.rb +205 -20
- data/lib/mcp/server_context.rb +22 -0
- data/lib/mcp/server_session.rb +50 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +3 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4773bb2a9b8bce187692737d82d8ba204da880a68255258bc1bc697ee994d356
|
|
4
|
+
data.tar.gz: e27c758cacba095342a7b8a7b1feb7299298773d0399676f5ee3827322765fd5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83c6b8ab2f63916f03f6d79a5e0f8e572f828d5153f0ee3dcb43e002c97b46a9e686959dd0e8e46a620b5155b456739a4ffd78dc51093898c56ddf2c6a21f99c
|
|
7
|
+
data.tar.gz: 46687cedce187961437298f5d9d4a807e058099a8c8e4f7793473999eb79a4dc70cf3a9cceaad8a3c8df4c20e4bebc85e7995844f413e64c6d4109918ffa856f
|
data/README.md
CHANGED
|
@@ -46,6 +46,8 @@ 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 draft, SEP-2575): returns `supportedVersions`, `capabilities`, `serverInfo`,
|
|
50
|
+
and `instructions`, and responds before `initialize` and without an `Mcp-Session-Id`
|
|
49
51
|
- `ping` - Simple health check
|
|
50
52
|
- `logging/setLevel` - Configures the minimum log level for the server
|
|
51
53
|
- `tools/list` - Lists all registered tools and their schemas
|
|
@@ -102,6 +104,8 @@ transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
|
102
104
|
transport.open
|
|
103
105
|
```
|
|
104
106
|
|
|
107
|
+
`StdioTransport.new` accepts an optional `max_line_bytes:` keyword that caps the byte length of a single newline-delimited request frame. A frame that reaches this limit without a newline is rejected and the connection is closed, preventing unbounded memory growth from a peer that never emits a newline. It defaults to `4 * 1024 * 1024` (4 MiB).
|
|
108
|
+
|
|
105
109
|
You can run this script and then type in requests to the server at the command line.
|
|
106
110
|
|
|
107
111
|
```console
|
|
@@ -128,6 +132,27 @@ The following examples show two common integration styles in Rails.
|
|
|
128
132
|
>
|
|
129
133
|
> Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
|
|
130
134
|
|
|
135
|
+
> [!IMPORTANT]
|
|
136
|
+
> Per MCP 2025-11-25, `StreamableHTTPTransport` validates the `Host` and `Origin` headers by default to
|
|
137
|
+
> prevent DNS rebinding attacks against locally bound servers, rejecting unauthorized values with HTTP 403.
|
|
138
|
+
> `Host` is allowed for the loopback defaults (`127.0.0.1`, `::1`, `localhost`), and an `Origin` header,
|
|
139
|
+
> when present, must be same-origin or explicitly allow-listed. Non-browser clients that send no `Origin`
|
|
140
|
+
> header are unaffected.
|
|
141
|
+
>
|
|
142
|
+
> Deployments behind a reverse proxy or bound to a non-loopback interface must widen the allow lists:
|
|
143
|
+
>
|
|
144
|
+
> ```ruby
|
|
145
|
+
> transport = MCP::Server::Transports::StreamableHTTPTransport.new(
|
|
146
|
+
> server,
|
|
147
|
+
> allowed_hosts: ["mcp.example.com"],
|
|
148
|
+
> allowed_origins: ["https://app.example.com"],
|
|
149
|
+
> )
|
|
150
|
+
> ```
|
|
151
|
+
>
|
|
152
|
+
> An `allowed_hosts:` entry matches either the bare host name (any port) or the full `host:port` value,
|
|
153
|
+
> so both `"mcp.example.com"` and `"mcp.example.com:8443"` work. Pass `dns_rebinding_protection: false`
|
|
154
|
+
> to disable the check entirely (e.g., when an upstream proxy or middleware already validates `Host`/`Origin`).
|
|
155
|
+
|
|
131
156
|
##### Rails (mount)
|
|
132
157
|
|
|
133
158
|
`StreamableHTTPTransport` is a Rack app that can be mounted directly in Rails routes:
|
|
@@ -155,6 +180,8 @@ end
|
|
|
155
180
|
the [MCP Streamable HTTP transport spec](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http),
|
|
156
181
|
so no additional route configuration is needed.
|
|
157
182
|
|
|
183
|
+
A complete runnable application using this approach is available in [`examples/rails`](examples/rails).
|
|
184
|
+
|
|
158
185
|
##### Rails (controller)
|
|
159
186
|
|
|
160
187
|
While the mount approach creates a single server at boot time, the controller approach creates a new server per request.
|
|
@@ -256,6 +283,46 @@ On the client, pass extensions through `connect`:
|
|
|
256
283
|
client.connect(capabilities: { extensions: { "com.example/feature" => {} } })
|
|
257
284
|
```
|
|
258
285
|
|
|
286
|
+
### MCP Apps (SEP-1865)
|
|
287
|
+
|
|
288
|
+
MCP Apps is a Final extension (negotiated via the Capability Extensions mechanism above) that lets a server ship interactive
|
|
289
|
+
HTML user interfaces which the host renders for tool results. On the server side the extension is a thin convention,
|
|
290
|
+
and `MCP::Apps` provides the vocabulary and helpers:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
capabilities = MCP::Server::Capabilities.new
|
|
294
|
+
capabilities.support_tools
|
|
295
|
+
capabilities.support_resources
|
|
296
|
+
capabilities.support_extensions(MCP::Apps.capability) # { "io.modelcontextprotocol/ui" => { mimeTypes: [...] } }
|
|
297
|
+
|
|
298
|
+
server = MCP::Server.new(
|
|
299
|
+
name: "weather_server",
|
|
300
|
+
capabilities: capabilities,
|
|
301
|
+
# UI templates are ordinary resources with a `ui://` URI and the `text/html;profile=mcp-app` MIME type.
|
|
302
|
+
resources: [MCP::Apps.ui_resource(uri: "ui://weather-server/dashboard", name: "weather_dashboard")],
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
server.resources_read_handler do |params|
|
|
306
|
+
[{ uri: params[:uri], mimeType: MCP::Apps::RESOURCE_MIME_TYPE, text: "<html>...</html>" }]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Link the tool to its template via `_meta.ui.resourceUri` (pass `legacy: true` to also
|
|
310
|
+
# emit the older flat `"ui/resourceUri"` alias for hosts that predate the Final spec).
|
|
311
|
+
server.define_tool(
|
|
312
|
+
name: "get_weather",
|
|
313
|
+
meta: MCP::Apps.tool_meta(resource_uri: "ui://weather-server/dashboard"),
|
|
314
|
+
) do |server_context:|
|
|
315
|
+
# The extension is optional: always return a meaningful text result, and use
|
|
316
|
+
# `MCP::Apps.client_supports?` when UI-capable clients should get richer structured content.
|
|
317
|
+
MCP::Apps.client_supports?(server.client_capabilities) # => true when the host declared the extension
|
|
318
|
+
MCP::Tool::Response.new([{ type: "text", text: "Sunny, 22 degrees Celsius" }])
|
|
319
|
+
end
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Everything else the extension defines (the sandboxed iframe, the `ui/*` postMessage bridge, consent for UI-initiated actions)
|
|
323
|
+
is the HOST's responsibility; a server only ever receives ordinary `resources/read` and `tools/call` requests.
|
|
324
|
+
See the [MCP Apps specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx).
|
|
325
|
+
|
|
259
326
|
### Server Context and Configuration Block Data
|
|
260
327
|
|
|
261
328
|
#### `server_context`
|
|
@@ -949,7 +1016,71 @@ MCP spec includes [Resources](https://modelcontextprotocol.io/specification/late
|
|
|
949
1016
|
|
|
950
1017
|
### Reading Resources
|
|
951
1018
|
|
|
952
|
-
|
|
1019
|
+
Like tools and prompts, resources can be defined in three ways.
|
|
1020
|
+
|
|
1021
|
+
1. As a class that inherits from `MCP::Resource`, implementing `contents` to serve the resource body:
|
|
1022
|
+
|
|
1023
|
+
```ruby
|
|
1024
|
+
class MyResource < MCP::Resource
|
|
1025
|
+
uri "https://example.com/my_resource"
|
|
1026
|
+
resource_name "my-resource"
|
|
1027
|
+
title "My Resource"
|
|
1028
|
+
description "Lorem ipsum dolor sit amet"
|
|
1029
|
+
mime_type "text/html"
|
|
1030
|
+
|
|
1031
|
+
class << self
|
|
1032
|
+
def contents
|
|
1033
|
+
[MCP::Resource::TextContents.new(
|
|
1034
|
+
uri: uri,
|
|
1035
|
+
mime_type: mime_type,
|
|
1036
|
+
text: "Hello from example resource!"
|
|
1037
|
+
)]
|
|
1038
|
+
end
|
|
1039
|
+
end
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
server = MCP::Server.new(
|
|
1043
|
+
name: "my_server",
|
|
1044
|
+
resources: [MyResource],
|
|
1045
|
+
)
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
`resources/read` requests are routed automatically: when the requested URI matches a registered
|
|
1049
|
+
class-based resource, its `contents` method is called. `contents` may return an array of
|
|
1050
|
+
`MCP::Resource::TextContents` / `MCP::Resource::BlobContents` objects (or plain hashes), or a single one.
|
|
1051
|
+
Like tools, `contents` can opt in to a `server_context:` keyword argument to receive per-request context.
|
|
1052
|
+
|
|
1053
|
+
When class-based resources or resource templates are registered and a `resources/read` request
|
|
1054
|
+
does not match any of them, the server responds with the standard JSON-RPC Invalid Params error
|
|
1055
|
+
(`-32602`) carrying the requested URI in the error `data` member, per SEP-2164.
|
|
1056
|
+
|
|
1057
|
+
2. With the `MCP::Resource.define` method, whose block implements `contents`:
|
|
1058
|
+
|
|
1059
|
+
```ruby
|
|
1060
|
+
resource = MCP::Resource.define(
|
|
1061
|
+
uri: "https://example.com/my_resource",
|
|
1062
|
+
name: "my-resource",
|
|
1063
|
+
mime_type: "text/html",
|
|
1064
|
+
) do
|
|
1065
|
+
[MCP::Resource::TextContents.new(uri: uri, mime_type: mime_type, text: "Hello!")]
|
|
1066
|
+
end
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
3. Using the `MCP::Server#define_resource` method:
|
|
1070
|
+
|
|
1071
|
+
```ruby
|
|
1072
|
+
server = MCP::Server.new(name: "my_server")
|
|
1073
|
+
server.define_resource(
|
|
1074
|
+
uri: "https://example.com/my_resource",
|
|
1075
|
+
name: "my-resource",
|
|
1076
|
+
mime_type: "text/html",
|
|
1077
|
+
) do
|
|
1078
|
+
[MCP::Resource::TextContents.new(uri: "https://example.com/my_resource", mime_type: "text/html", text: "Hello!")]
|
|
1079
|
+
end
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
Alternatively, resources can be registered as plain data objects with `MCP::Resource.new`,
|
|
1083
|
+
in which case the server only lists them:
|
|
953
1084
|
|
|
954
1085
|
```ruby
|
|
955
1086
|
resource = MCP::Resource.new(
|
|
@@ -966,7 +1097,8 @@ server = MCP::Server.new(
|
|
|
966
1097
|
)
|
|
967
1098
|
```
|
|
968
1099
|
|
|
969
|
-
|
|
1100
|
+
With plain data resources, the server must register a handler for the `resources/read` method to
|
|
1101
|
+
retrieve a resource dynamically.
|
|
970
1102
|
|
|
971
1103
|
```ruby
|
|
972
1104
|
server.resources_read_handler do |params|
|
|
@@ -978,7 +1110,8 @@ server.resources_read_handler do |params|
|
|
|
978
1110
|
end
|
|
979
1111
|
```
|
|
980
1112
|
|
|
981
|
-
otherwise `resources/read` requests will be a no-op.
|
|
1113
|
+
otherwise `resources/read` requests will be a no-op. Note that a `resources_read_handler` fully replaces
|
|
1114
|
+
the default `resources/read` handling, including the automatic routing to class-based resources described above.
|
|
982
1115
|
|
|
983
1116
|
For unknown URIs, raise `MCP::Server::ResourceNotFoundError` from the handler.
|
|
984
1117
|
Per SEP-2164, the server then responds with the standard JSON-RPC Invalid Params error (`-32602`)
|
|
@@ -995,7 +1128,63 @@ end
|
|
|
995
1128
|
|
|
996
1129
|
### Resource Templates
|
|
997
1130
|
|
|
998
|
-
|
|
1131
|
+
Resource templates follow the same pattern. Class-based templates declare a `uri_template` and
|
|
1132
|
+
receive the variables extracted from the requested URI as keyword arguments to `contents`:
|
|
1133
|
+
|
|
1134
|
+
```ruby
|
|
1135
|
+
class UserProfileTemplate < MCP::ResourceTemplate
|
|
1136
|
+
uri_template "users://{user_id}/profile"
|
|
1137
|
+
resource_template_name "user-profile"
|
|
1138
|
+
title "User Profile"
|
|
1139
|
+
description "Profile data for a user"
|
|
1140
|
+
mime_type "application/json"
|
|
1141
|
+
|
|
1142
|
+
class << self
|
|
1143
|
+
def contents(user_id:)
|
|
1144
|
+
[MCP::Resource::TextContents.new(
|
|
1145
|
+
uri: "users://#{user_id}/profile",
|
|
1146
|
+
mime_type: mime_type,
|
|
1147
|
+
text: { id: user_id }.to_json
|
|
1148
|
+
)]
|
|
1149
|
+
end
|
|
1150
|
+
end
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
server = MCP::Server.new(
|
|
1154
|
+
name: "my_server",
|
|
1155
|
+
resource_templates: [UserProfileTemplate],
|
|
1156
|
+
)
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
A `resources/read` request for `users://42/profile` calls `UserProfileTemplate.contents(user_id: "42")`.
|
|
1160
|
+
An exact match against a registered resource takes precedence over template matching.
|
|
1161
|
+
`contents` can also opt in to a `server_context:` keyword argument.
|
|
1162
|
+
|
|
1163
|
+
URI template matching supports simple [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) level 1 `{variable}` expressions only:
|
|
1164
|
+
|
|
1165
|
+
- Operator expressions such as `{+path}`, `{#fragment}`, or `{?query}` are treated as literal text and never match an expanded URI.
|
|
1166
|
+
- A variable matches one or more characters excluding `/`.
|
|
1167
|
+
- Extracted values are not percent-decoded.
|
|
1168
|
+
|
|
1169
|
+
The `MCP::ResourceTemplate.define` and `MCP::Server#define_resource_template` methods are also available,
|
|
1170
|
+
mirroring the resource variants:
|
|
1171
|
+
|
|
1172
|
+
```ruby
|
|
1173
|
+
server.define_resource_template(
|
|
1174
|
+
uri_template: "users://{user_id}/profile",
|
|
1175
|
+
name: "user-profile",
|
|
1176
|
+
mime_type: "application/json",
|
|
1177
|
+
) do |user_id:|
|
|
1178
|
+
[MCP::Resource::TextContents.new(
|
|
1179
|
+
uri: "users://#{user_id}/profile",
|
|
1180
|
+
mime_type: "application/json",
|
|
1181
|
+
text: { id: user_id }.to_json
|
|
1182
|
+
)]
|
|
1183
|
+
end
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
Resource templates can also be registered as plain data objects with `MCP::ResourceTemplate.new`,
|
|
1187
|
+
in which case reads must be served by a `resources_read_handler`:
|
|
999
1188
|
|
|
1000
1189
|
```ruby
|
|
1001
1190
|
resource_template = MCP::ResourceTemplate.new(
|
|
@@ -1023,6 +1212,12 @@ Roots define the boundaries of where a server can operate, providing a list of d
|
|
|
1023
1212
|
- **Client Capability**: Clients must declare `roots` capability during initialization
|
|
1024
1213
|
- **Change Notifications**: Clients that support `roots.listChanged` send `notifications/roots/list_changed` when roots change
|
|
1025
1214
|
|
|
1215
|
+
> [!NOTE]
|
|
1216
|
+
> Per SEP-2260, server-to-client requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) must be associated with
|
|
1217
|
+
> an originating client request (`ping` is exempt). Use the `server_context` passed to your handler, which stamps the association
|
|
1218
|
+
> automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding
|
|
1219
|
+
> `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning.
|
|
1220
|
+
|
|
1026
1221
|
**Using Roots in Tools:**
|
|
1027
1222
|
|
|
1028
1223
|
Tools that accept a `server_context:` parameter can call `list_roots` on it.
|
|
@@ -1758,12 +1953,58 @@ Session-scoped standalone notifications (`resources/updated`, `elicitation/compl
|
|
|
1758
1953
|
broadcast notifications (`tools/list_changed`, etc.) still flow to clients connected to the GET SSE stream.
|
|
1759
1954
|
This mode is suitable for simple tool servers that do not need server-initiated requests.
|
|
1760
1955
|
|
|
1761
|
-
By default, sessions
|
|
1762
|
-
|
|
1956
|
+
By default, stateful sessions are bounded so an `initialize` flood cannot retain sessions until memory is exhausted:
|
|
1957
|
+
they expire after `session_idle_timeout` seconds of inactivity (default 1800, i.e. 30 minutes) and the concurrent
|
|
1958
|
+
session count is capped at `max_sessions` (default 10000). A session's idle timer is reset by activity that touches it
|
|
1959
|
+
(a GET, or a regular-request POST), and expired sessions are collected by a background reaper roughly once a minute,
|
|
1960
|
+
so cleanup lags inactivity by up to that interval. At the cap, the transport first reclaims any already-expired slots
|
|
1961
|
+
and then, if still full, rejects a new `initialize` with HTTP 503 (it does not evict an existing session).
|
|
1962
|
+
|
|
1963
|
+
```ruby
|
|
1964
|
+
# Tune the limits
|
|
1965
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 900, max_sessions: 5000)
|
|
1966
|
+
|
|
1967
|
+
# Opt out of expiry and/or the cap (not recommended on internet-facing deployments)
|
|
1968
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: nil, max_sessions: nil)
|
|
1969
|
+
```
|
|
1970
|
+
|
|
1971
|
+
Stateless mode (`stateless: true`) retains no sessions, so neither limit applies to it.
|
|
1972
|
+
|
|
1973
|
+
#### Session Ownership
|
|
1974
|
+
|
|
1975
|
+
`StreamableHTTPTransport` issues a random `SecureRandom.uuid` session ID and validates incoming requests by session
|
|
1976
|
+
existence and idle timeout only. It does not bind a session to a user, because the transport never receives
|
|
1977
|
+
an authenticated identity on its own. A caller that obtains a valid session ID could therefore act on that session,
|
|
1978
|
+
so binding a session to a user is the deploying application's responsibility (the MCP spec frames this as a SHOULD).
|
|
1979
|
+
|
|
1980
|
+
The primary control is the `session_request_validator`. It is called as `->(request, session_id) { true | false }`
|
|
1981
|
+
on every non-`initialize` POST, GET, and DELETE against an existing session (including notification and response POSTs,
|
|
1982
|
+
so a stolen session ID cannot, for example, POST `notifications/cancelled` against a victim's request). A falsy return
|
|
1983
|
+
rejects the request with HTTP 403. Use it to compare the request's authenticated principal against the one recorded
|
|
1984
|
+
when the session was created:
|
|
1985
|
+
|
|
1986
|
+
```ruby
|
|
1987
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
|
|
1988
|
+
server,
|
|
1989
|
+
session_request_validator: ->(request, session_id) { owns_session?(request, session_id) },
|
|
1990
|
+
)
|
|
1991
|
+
```
|
|
1992
|
+
|
|
1993
|
+
Without a validator the transport does not enforce ownership. As a limited defense in depth (not authentication),
|
|
1994
|
+
it also records the `Origin` header at `initialize` and rejects a later request whose `Origin` differs, but only
|
|
1995
|
+
when both are present - a non-browser client that omits `Origin` (e.g. `curl` or a script) is not stopped by this check.
|
|
1996
|
+
Enforcing ownership against a determined attacker requires supplying the validator with an authenticated principal.
|
|
1997
|
+
|
|
1998
|
+
#### Request Size Limits
|
|
1999
|
+
|
|
2000
|
+
`StreamableHTTPTransport` bounds how many bytes a single POST body may allocate, so a peer cannot exhaust memory
|
|
2001
|
+
with one oversized message. A body larger than `max_request_bytes` (default 4 MiB) is rejected with HTTP 413,
|
|
2002
|
+
and JSON nesting depth is capped. The 4 MiB default comfortably fits a typical JSON-RPC message (a 4 MiB JSON
|
|
2003
|
+
string decodes to roughly 3 MiB of base64 payload) and matches the TypeScript SDK's 4 MB default; raise it only
|
|
2004
|
+
if you exchange unusually large payloads:
|
|
1763
2005
|
|
|
1764
2006
|
```ruby
|
|
1765
|
-
|
|
1766
|
-
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
|
|
2007
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, max_request_bytes: 8 * 1024 * 1024)
|
|
1767
2008
|
```
|
|
1768
2009
|
|
|
1769
2010
|
### Pagination
|
|
@@ -1850,6 +2091,40 @@ tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
|
|
|
1850
2091
|
Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
|
|
1851
2092
|
fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
|
|
1852
2093
|
|
|
2094
|
+
#### List Result Caching (`ttlMs` / `cacheScope`)
|
|
2095
|
+
|
|
2096
|
+
Per SEP-2549, list and read results can carry cache hints telling clients how long a result stays fresh (`ttlMs`, max-age semantics in milliseconds;
|
|
2097
|
+
`0` means do not cache) and whether shared intermediaries may cache it (`cacheScope`: `"public"` or `"private"`).
|
|
2098
|
+
|
|
2099
|
+
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: "public"`).
|
|
2101
|
+
When neither is set, responses are serialized exactly as before.
|
|
2102
|
+
|
|
2103
|
+
```ruby
|
|
2104
|
+
server = MCP::Server.new(
|
|
2105
|
+
name: "my_server",
|
|
2106
|
+
tools: tools,
|
|
2107
|
+
ttl_ms: 60_000, # results stay fresh for one minute
|
|
2108
|
+
cache_scope: "private", # only the requesting client may cache them
|
|
2109
|
+
)
|
|
2110
|
+
```
|
|
2111
|
+
|
|
2112
|
+
A `resources_read_handler` can override the hints per result by returning a full result hash instead of bare contents:
|
|
2113
|
+
|
|
2114
|
+
```ruby
|
|
2115
|
+
server.resources_read_handler do |params|
|
|
2116
|
+
{ contents: [{ uri: params[:uri], mimeType: "text/plain", text: "..." }], ttlMs: 5_000 }
|
|
2117
|
+
end
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
On the client, the values are surfaced on the paginated result structs as `ttl_ms` and `cache_scope`:
|
|
2121
|
+
|
|
2122
|
+
```ruby
|
|
2123
|
+
page = client.list_tools
|
|
2124
|
+
page.ttl_ms # => 60000 (nil when the server sent no hint)
|
|
2125
|
+
page.cache_scope # => "private"
|
|
2126
|
+
```
|
|
2127
|
+
|
|
1853
2128
|
### Advanced
|
|
1854
2129
|
|
|
1855
2130
|
#### Custom Methods
|
|
@@ -1958,6 +2233,7 @@ Use the `MCP::Client::Stdio` transport to interact with MCP servers running as s
|
|
|
1958
2233
|
| `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
|
|
1959
2234
|
| `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
|
|
1960
2235
|
| `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
|
|
2236
|
+
| `max_line_bytes:` | No | Maximum byte length of a single newline-delimited response frame. A frame that reaches this limit without a newline is rejected as a transport error, preventing unbounded memory growth from a server that never emits a newline. Defaults to `4 * 1024 * 1024` (4 MiB). |
|
|
1961
2237
|
|
|
1962
2238
|
Example usage:
|
|
1963
2239
|
|
|
@@ -2042,6 +2318,30 @@ response = client.call_tool(
|
|
|
2042
2318
|
|
|
2043
2319
|
The server will send `notifications/progress` back to the client during execution.
|
|
2044
2320
|
|
|
2321
|
+
#### Server-to-Client Requests (Elicitation)
|
|
2322
|
+
|
|
2323
|
+
Servers can send requests back to the client while one of the client's own requests is in flight - for example,
|
|
2324
|
+
[`elicitation/create`](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) to ask the user for additional input during a tool call.
|
|
2325
|
+
Register a handler and advertise the capability on `connect` to respond to them:
|
|
2326
|
+
|
|
2327
|
+
```ruby
|
|
2328
|
+
client.connect(capabilities: { elicitation: {} })
|
|
2329
|
+
|
|
2330
|
+
client.on_elicitation do |params|
|
|
2331
|
+
{
|
|
2332
|
+
action: "accept",
|
|
2333
|
+
# Fill fields omitted by the user with the schema's `default` values (SEP-1034)
|
|
2334
|
+
content: MCP::Client::Elicitation.apply_defaults(params["requestedSchema"]),
|
|
2335
|
+
}
|
|
2336
|
+
end
|
|
2337
|
+
```
|
|
2338
|
+
|
|
2339
|
+
Registering a handler opens a standalone HTTP GET SSE stream on a background thread
|
|
2340
|
+
([listening for messages from the server](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server)),
|
|
2341
|
+
since servers deliver requests that are not tied to a client request on that stream. Server requests with no registered handler are answered with
|
|
2342
|
+
a JSON-RPC `-32601` (method not found) error. To handle methods other than `elicitation/create`, register directly on the transport with
|
|
2343
|
+
`http_transport.on_server_request("method/name") { |params| ... }`.
|
|
2344
|
+
|
|
2045
2345
|
#### HTTP Authorization
|
|
2046
2346
|
|
|
2047
2347
|
By default, the HTTP transport layer provides no authentication to the server, but you can provide custom headers if you need authentication. For example, to use Bearer token authentication:
|
|
@@ -2118,13 +2418,17 @@ Required keyword arguments to `Provider.new`:
|
|
|
2118
2418
|
an explicit value always wins.
|
|
2119
2419
|
- `redirect_uri`: String. Must use HTTPS or be a loopback URL (`localhost`, `127.0.0.0/8`, `::1`); other values raise `Provider::InsecureRedirectURIError`.
|
|
2120
2420
|
- `redirect_handler`: Callable invoked with the fully-built authorization `URI`. Typically opens the user's browser.
|
|
2121
|
-
- `callback_handler`: Callable that returns `[code, state]` after the user is redirected back to `redirect_uri`.
|
|
2421
|
+
- `callback_handler`: Callable that returns `[code, state]` or `[code, state, iss]` after the user is redirected back to `redirect_uri`. Returning the 3-element form
|
|
2422
|
+
(with `iss` set to the RFC 9207 `iss` parameter from the redirect, or `nil` when absent) opts into SEP-2468 issuer validation: a present `iss` must match
|
|
2423
|
+
the authorization server's issuer, and a missing one is rejected when the server advertises `authorization_response_iss_parameter_supported`.
|
|
2122
2424
|
|
|
2123
2425
|
Optional keyword arguments:
|
|
2124
2426
|
|
|
2125
2427
|
- `scope`: Space-separated scopes to request when the server's `WWW-Authenticate` does not specify one.
|
|
2126
2428
|
- `storage`: Object responding to `tokens`, `save_tokens(t)`, `client_information`, `save_client_information(info)`. Defaults to `MCP::Client::OAuth::InMemoryStorage`,
|
|
2127
|
-
which keeps credentials in process memory only.
|
|
2429
|
+
which keeps credentials in process memory only. Persisted `client_information` is stamped with an `"issuer"` member binding it to the authorization server that
|
|
2430
|
+
issued it (SEP-2352): when the server's authorization server changes, the SDK discards the stale registration and its tokens and re-registers automatically
|
|
2431
|
+
(portable CIMD `client_id`s are kept). Treat the hash as opaque and persist it as-is.
|
|
2128
2432
|
- `client_id_metadata_document_url`: URL where you publish a Client ID Metadata Document
|
|
2129
2433
|
(`draft-ietf-oauth-client-id-metadata-document` and the MCP authorization specification).
|
|
2130
2434
|
When the authorization server advertises `client_id_metadata_document_supported: true`,
|
|
@@ -2232,6 +2536,16 @@ The client provides a wrapper class for tools returned by the server:
|
|
|
2232
2536
|
|
|
2233
2537
|
This class provides easy access to tool properties like name, description, input schema, and output schema.
|
|
2234
2538
|
|
|
2539
|
+
### Multi-Round-Trip Results (Experimental, SEP-2322)
|
|
2540
|
+
|
|
2541
|
+
The MCP 2026-07-28 draft replaces in-flight server-to-client requests with Multi Round-Trip Requests: instead of issuing `sampling/createMessage`, `roots/list`,
|
|
2542
|
+
or `elicitation/create` while a request is being processed, a server may answer with a result whose `resultType` is `"input_required"`, carrying an `inputRequests` map
|
|
2543
|
+
and an opaque `requestState`; the client fulfills the requests and re-issues the original request with `inputResponses` and the echoed `requestState`.
|
|
2544
|
+
|
|
2545
|
+
The Ruby client recognizes such results and raises `MCP::Client::InputRequiredError` instead of returning them as if they were final. The error exposes `input_requests`, `request_state`,
|
|
2546
|
+
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`
|
|
2547
|
+
are provided for forward compatibility. Servers on stable protocol versions never send `resultType`, so existing behavior is unchanged.
|
|
2548
|
+
|
|
2235
2549
|
## Conformance Testing
|
|
2236
2550
|
|
|
2237
2551
|
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
|
@@ -63,12 +63,29 @@ module JsonRpcHandler
|
|
|
63
63
|
message: "Parse error",
|
|
64
64
|
data: "Invalid JSON",
|
|
65
65
|
})
|
|
66
|
+
rescue StandardError
|
|
67
|
+
# Last-resort guard so an unexpected handling error returns a JSON-RPC error
|
|
68
|
+
# instead of escaping to the transport loop.
|
|
69
|
+
response = error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
|
|
70
|
+
code: ErrorCode::INTERNAL_ERROR,
|
|
71
|
+
message: "Internal error",
|
|
72
|
+
})
|
|
66
73
|
end
|
|
67
74
|
|
|
68
75
|
response&.to_json
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
def process_request(request, id_validation_pattern:, &method_finder)
|
|
79
|
+
# A batch element can be any JSON value; a non-object element cannot carry an id or a method,
|
|
80
|
+
# so reject it before the `request[:id]` access below.
|
|
81
|
+
unless request.is_a?(Hash)
|
|
82
|
+
return error_response(id: :unknown_id, id_validation_pattern: id_validation_pattern, error: {
|
|
83
|
+
code: ErrorCode::INVALID_REQUEST,
|
|
84
|
+
message: "Invalid Request",
|
|
85
|
+
data: "Request must be an object",
|
|
86
|
+
})
|
|
87
|
+
end
|
|
88
|
+
|
|
72
89
|
id = request[:id]
|
|
73
90
|
|
|
74
91
|
error = if !valid_version?(request[:jsonrpc])
|
data/lib/mcp/apps.rb
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
# Server-side helpers for the MCP Apps extension (SEP-1865, Extensions Track, Final):
|
|
5
|
+
# interactive user interfaces delivered as `ui://` HTML template resources that hosts
|
|
6
|
+
# render for tool results. The extension is negotiated per SEP-2133 through
|
|
7
|
+
# `capabilities.extensions` on both sides; a server declares {EXTENSION_ID}, registers
|
|
8
|
+
# UI templates as ordinary resources, and links tools to templates via `_meta`.
|
|
9
|
+
#
|
|
10
|
+
# Everything else defined by the extension (the sandboxed iframe, the `ui/*`
|
|
11
|
+
# postMessage bridge methods, consent for UI-initiated calls) is HOST responsibility:
|
|
12
|
+
# a server only ever receives ordinary `resources/read` and `tools/call` requests.
|
|
13
|
+
#
|
|
14
|
+
# Because the extension is optional, a UI-enabled tool MUST still return a meaningful
|
|
15
|
+
# text-only result for clients that did not declare the capability;
|
|
16
|
+
# use {Apps.client_supports?} to branch.
|
|
17
|
+
#
|
|
18
|
+
# @example Declaring an Apps-enabled server
|
|
19
|
+
# capabilities = MCP::Server::Capabilities.new
|
|
20
|
+
# capabilities.support_tools
|
|
21
|
+
# capabilities.support_resources
|
|
22
|
+
# capabilities.support_extensions(MCP::Apps.capability)
|
|
23
|
+
# server = MCP::Server.new(
|
|
24
|
+
# name: "weather_server",
|
|
25
|
+
# capabilities: capabilities,
|
|
26
|
+
# resources: [MCP::Apps.ui_resource(uri: "ui://weather-server/dashboard", name: "weather_dashboard")],
|
|
27
|
+
# )
|
|
28
|
+
# server.define_tool(
|
|
29
|
+
# name: "get_weather",
|
|
30
|
+
# meta: MCP::Apps.tool_meta(resource_uri: "ui://weather-server/dashboard"),
|
|
31
|
+
# ) { |server_context:|
|
|
32
|
+
# ...
|
|
33
|
+
# }
|
|
34
|
+
#
|
|
35
|
+
# https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
|
|
36
|
+
module Apps
|
|
37
|
+
# Reverse-DNS extension identifier (note: `/ui`, not `/apps`), shared wire vocabulary with
|
|
38
|
+
# the reference `@modelcontextprotocol/ext-apps` package and the Python SDK's `Apps` extension.
|
|
39
|
+
EXTENSION_ID = "io.modelcontextprotocol/ui"
|
|
40
|
+
# UI template resources MUST use this parameterized MIME type.
|
|
41
|
+
RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"
|
|
42
|
+
# UI template resource URIs MUST use this scheme.
|
|
43
|
+
URI_SCHEME = "ui://"
|
|
44
|
+
# Legacy flat `_meta` key linking a tool to its template; the canonical shape is
|
|
45
|
+
# the nested `_meta.ui.resourceUri`. {Apps.tool_meta} can emit both.
|
|
46
|
+
RESOURCE_URI_META_KEY = "ui/resourceUri"
|
|
47
|
+
|
|
48
|
+
extend self
|
|
49
|
+
|
|
50
|
+
# The `capabilities.extensions` fragment advertising Apps support. Pass to
|
|
51
|
+
# `MCP::Server::Capabilities.new(extensions: ...)` or `support_extensions(...)`,
|
|
52
|
+
# or merge into a client's `connect(capabilities: { extensions: ... })`.
|
|
53
|
+
def capability(mime_types: [RESOURCE_MIME_TYPE])
|
|
54
|
+
{ EXTENSION_ID => { mimeTypes: mime_types } }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Builds an `MCP::Resource` for a UI template, enforcing the spec's MUSTs:
|
|
58
|
+
# a `ui://` URI and the `text/html;profile=mcp-app` MIME type by default.
|
|
59
|
+
def ui_resource(uri:, name:, mime_type: RESOURCE_MIME_TYPE, **rest)
|
|
60
|
+
unless uri.is_a?(String) && uri.start_with?(URI_SCHEME)
|
|
61
|
+
raise ArgumentError, "MCP Apps template URIs must start with #{URI_SCHEME.inspect} (got #{uri.inspect})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Resource.new(uri: uri, name: name, mime_type: mime_type, **rest)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Builds the tool `_meta` linking a tool to its UI template, merged non-destructively into
|
|
68
|
+
# caller-supplied `meta`. `visibility` restricts who sees the tool (an array of `"model"` / `"app"`).
|
|
69
|
+
# `legacy: true` also writes the flat `"ui/resourceUri"` alias, matching the reference server helper
|
|
70
|
+
# that keeps both key shapes in sync for older hosts.
|
|
71
|
+
def tool_meta(resource_uri:, visibility: nil, meta: nil, legacy: false)
|
|
72
|
+
unless resource_uri.is_a?(String) && resource_uri.start_with?(URI_SCHEME)
|
|
73
|
+
raise ArgumentError, "resource_uri must start with #{URI_SCHEME.inspect} (got #{resource_uri.inspect})"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
ui_entry = { resourceUri: resource_uri }
|
|
77
|
+
ui_entry[:visibility] = visibility if visibility
|
|
78
|
+
|
|
79
|
+
merged = (meta || {}).merge(ui: ui_entry)
|
|
80
|
+
merged[RESOURCE_URI_META_KEY.to_sym] = resource_uri if legacy
|
|
81
|
+
merged
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Whether the client declared Apps support for `mime_type` in its `capabilities.extensions`
|
|
85
|
+
# (symbol or string keys). UI-enabled tools use this to fall back to a text-only result for
|
|
86
|
+
# clients without the extension.
|
|
87
|
+
def client_supports?(client_capabilities, mime_type: RESOURCE_MIME_TYPE)
|
|
88
|
+
extensions = read_key(client_capabilities, :extensions)
|
|
89
|
+
declaration = read_key(extensions, EXTENSION_ID)
|
|
90
|
+
return false unless declaration.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
mime_types = read_key(declaration, :mimeTypes)
|
|
93
|
+
|
|
94
|
+
# A declaration without mimeTypes advertises the extension without narrowing.
|
|
95
|
+
return true if mime_types.nil?
|
|
96
|
+
|
|
97
|
+
mime_types.is_a?(Array) && mime_types.include?(mime_type)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def read_key(hash, key)
|
|
103
|
+
return unless hash.is_a?(Hash)
|
|
104
|
+
|
|
105
|
+
value = hash[key.to_sym]
|
|
106
|
+
value.nil? ? hash[key.to_s] : value
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Client
|
|
5
|
+
# Client-side helpers for the `elicitation/create` server-to-client request.
|
|
6
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation
|
|
7
|
+
module Elicitation
|
|
8
|
+
# Fills fields omitted from `content` with the `default` values declared in
|
|
9
|
+
# the elicitation request's `requestedSchema` properties, per SEP-1034.
|
|
10
|
+
# Provided values are never overwritten, and properties without a `default`
|
|
11
|
+
# are left out so the server can apply its own handling.
|
|
12
|
+
# Mirrors the TypeScript SDK's `applyElicitationDefaults`; the Python SDK
|
|
13
|
+
# applies defaults in the elicitation callback the same way this helper
|
|
14
|
+
# is intended to be used.
|
|
15
|
+
#
|
|
16
|
+
# @param requested_schema [Hash] The `requestedSchema` from the `elicitation/create`
|
|
17
|
+
# request params (string or symbol keys).
|
|
18
|
+
# @param content [Hash] Values already collected from the user.
|
|
19
|
+
# @return [Hash] `content` (string keys) with defaults filled in.
|
|
20
|
+
#
|
|
21
|
+
# @example Accept an elicitation request with all defaults
|
|
22
|
+
# transport.on_server_request("elicitation/create") do |params|
|
|
23
|
+
# {
|
|
24
|
+
# action: "accept",
|
|
25
|
+
# content: MCP::Client::Elicitation.apply_defaults(params["requestedSchema"]),
|
|
26
|
+
# }
|
|
27
|
+
# end
|
|
28
|
+
class << self
|
|
29
|
+
def apply_defaults(requested_schema, content = {})
|
|
30
|
+
filled = content.to_h.transform_keys(&:to_s)
|
|
31
|
+
properties = requested_schema["properties"] || requested_schema[:properties]
|
|
32
|
+
return filled unless properties.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
properties.each do |name, property_schema|
|
|
35
|
+
name = name.to_s
|
|
36
|
+
next if filled.key?(name)
|
|
37
|
+
next unless property_schema.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
if property_schema.key?("default")
|
|
40
|
+
filled[name] = property_schema["default"]
|
|
41
|
+
elsif property_schema.key?(:default)
|
|
42
|
+
filled[name] = property_schema[:default]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
filled
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|