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 +4 -4
- data/README.md +250 -6
- 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 +3 -1
- data/lib/mcp/client.rb +89 -1
- data/lib/mcp/error_codes.rb +21 -0
- data/lib/mcp/methods.rb +3 -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/streamable_http_transport.rb +113 -78
- data/lib/mcp/server.rb +191 -20
- data/lib/mcp/server_context.rb +12 -0
- data/lib/mcp/server_session.rb +40 -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
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
|