mcp 0.23.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: b3d294b5e8b52b58ba3d2a2752db94f3a0d836ed6003d10ba0220d9d31b2d267
4
- data.tar.gz: 1f0e92d3a77fc36166a1f68c3443aa9d5174ee139491b784dfa3cdcb92caa8e1
3
+ metadata.gz: 4773bb2a9b8bce187692737d82d8ba204da880a68255258bc1bc697ee994d356
4
+ data.tar.gz: e27c758cacba095342a7b8a7b1feb7299298773d0399676f5ee3827322765fd5
5
5
  SHA512:
6
- metadata.gz: 975ebdaeab3e8dd8219c81c812fcbde232047a9baabf6924bea6fba7f4eae05b6eab96aa02efd601e83e443fc0ad62107f9379a1ab07924d1fcccedae48c6232
7
- data.tar.gz: 624de6afb8859bce866dabef20c6357913ff8adf2e479f84d2472049f7f7f2853dc65f27ed1925f1c3f76eae880a3886806136dea6d94459988d24e6f973d304
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
@@ -178,6 +180,8 @@ end
178
180
  the [MCP Streamable HTTP transport spec](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http),
179
181
  so no additional route configuration is needed.
180
182
 
183
+ A complete runnable application using this approach is available in [`examples/rails`](examples/rails).
184
+
181
185
  ##### Rails (controller)
182
186
 
183
187
  While the mount approach creates a single server at boot time, the controller approach creates a new server per request.
@@ -279,6 +283,46 @@ On the client, pass extensions through `connect`:
279
283
  client.connect(capabilities: { extensions: { "com.example/feature" => {} } })
280
284
  ```
281
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
+
282
326
  ### Server Context and Configuration Block Data
283
327
 
284
328
  #### `server_context`
@@ -972,7 +1016,71 @@ MCP spec includes [Resources](https://modelcontextprotocol.io/specification/late
972
1016
 
973
1017
  ### Reading Resources
974
1018
 
975
- 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:
976
1084
 
977
1085
  ```ruby
978
1086
  resource = MCP::Resource.new(
@@ -989,7 +1097,8 @@ server = MCP::Server.new(
989
1097
  )
990
1098
  ```
991
1099
 
992
- 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.
993
1102
 
994
1103
  ```ruby
995
1104
  server.resources_read_handler do |params|
@@ -1001,7 +1110,8 @@ server.resources_read_handler do |params|
1001
1110
  end
1002
1111
  ```
1003
1112
 
1004
- 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.
1005
1115
 
1006
1116
  For unknown URIs, raise `MCP::Server::ResourceNotFoundError` from the handler.
1007
1117
  Per SEP-2164, the server then responds with the standard JSON-RPC Invalid Params error (`-32602`)
@@ -1018,7 +1128,63 @@ end
1018
1128
 
1019
1129
  ### Resource Templates
1020
1130
 
1021
- 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`:
1022
1188
 
1023
1189
  ```ruby
1024
1190
  resource_template = MCP::ResourceTemplate.new(
@@ -1046,6 +1212,12 @@ Roots define the boundaries of where a server can operate, providing a list of d
1046
1212
  - **Client Capability**: Clients must declare `roots` capability during initialization
1047
1213
  - **Change Notifications**: Clients that support `roots.listChanged` send `notifications/roots/list_changed` when roots change
1048
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
+
1049
1221
  **Using Roots in Tools:**
1050
1222
 
1051
1223
  Tools that accept a `server_context:` parameter can call `list_roots` on it.
@@ -1919,6 +2091,40 @@ tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
1919
2091
  Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
1920
2092
  fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
1921
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
+
1922
2128
  ### Advanced
1923
2129
 
1924
2130
  #### Custom Methods
@@ -2112,6 +2318,30 @@ response = client.call_tool(
2112
2318
 
2113
2319
  The server will send `notifications/progress` back to the client during execution.
2114
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
+
2115
2345
  #### HTTP Authorization
2116
2346
 
2117
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:
@@ -2188,13 +2418,17 @@ Required keyword arguments to `Provider.new`:
2188
2418
  an explicit value always wins.
2189
2419
  - `redirect_uri`: String. Must use HTTPS or be a loopback URL (`localhost`, `127.0.0.0/8`, `::1`); other values raise `Provider::InsecureRedirectURIError`.
2190
2420
  - `redirect_handler`: Callable invoked with the fully-built authorization `URI`. Typically opens the user's browser.
2191
- - `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`.
2192
2424
 
2193
2425
  Optional keyword arguments:
2194
2426
 
2195
2427
  - `scope`: Space-separated scopes to request when the server's `WWW-Authenticate` does not specify one.
2196
2428
  - `storage`: Object responding to `tokens`, `save_tokens(t)`, `client_information`, `save_client_information(info)`. Defaults to `MCP::Client::OAuth::InMemoryStorage`,
2197
- 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.
2198
2432
  - `client_id_metadata_document_url`: URL where you publish a Client ID Metadata Document
2199
2433
  (`draft-ietf-oauth-client-id-metadata-document` and the MCP authorization specification).
2200
2434
  When the authorization server advertises `client_id_metadata_document_supported: true`,
@@ -2302,6 +2536,16 @@ The client provides a wrapper class for tools returned by the server:
2302
2536
 
2303
2537
  This class provides easy access to tool properties like name, description, input schema, and output schema.
2304
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
+
2305
2549
  ## Conformance Testing
2306
2550
 
2307
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