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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0e0cdbc945ee9ec5d178aeaedb1c8af84eca0bae8db8e264f523b47b8d67c52
4
- data.tar.gz: 6acb2d299d9ae215c26db733091ebbcbb3f6b1477c94d9b4047f9ab37c3a8f0c
3
+ metadata.gz: 4773bb2a9b8bce187692737d82d8ba204da880a68255258bc1bc697ee994d356
4
+ data.tar.gz: e27c758cacba095342a7b8a7b1feb7299298773d0399676f5ee3827322765fd5
5
5
  SHA512:
6
- metadata.gz: 8299a6cf0101d4b8b980a69ed3128636aae6f0497fb431d14fdc0f6b8b659fb18ab5d886cf1cb567c53f385a4f8c0cdaf5e91aff6d1a4fe4daefca25d17a7dc2
7
- data.tar.gz: dff4075f2a0fdf27329af3455b1db6960e38f557ea2436388f1a271d6915172e318b9630ccc8312fab16e3e8ac42e0dc84ef20e974b313056d377f3798d4a83c
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
- The `MCP::Resource` class provides a way to register resources with the server.
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
- The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
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
- The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
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 do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
1762
- When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
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
- # Session timeout of 30 minutes
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).
@@ -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