mcp 0.23.0 → 0.25.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 +329 -6
- data/lib/json_rpc_handler.rb +18 -1
- data/lib/mcp/apps.rb +109 -0
- data/lib/mcp/client/elicitation.rb +51 -0
- data/lib/mcp/client/http.rb +383 -32
- data/lib/mcp/client/oauth/client_credentials_provider.rb +72 -14
- data/lib/mcp/client/oauth/cross_app_access_provider.rb +80 -0
- data/lib/mcp/client/oauth/discovery.rb +1 -1
- data/lib/mcp/client/oauth/flow.rb +182 -6
- data/lib/mcp/client/oauth/id_jag_token_exchange.rb +101 -0
- 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 +5 -1
- data/lib/mcp/client/paginated_result.rb +7 -5
- data/lib/mcp/client/stdio.rb +3 -1
- data/lib/mcp/client.rb +127 -1
- data/lib/mcp/configuration.rb +1 -1
- data/lib/mcp/error_codes.rb +21 -0
- data/lib/mcp/icon.rb +1 -1
- data/lib/mcp/instrumentation.rb +0 -2
- 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 +138 -87
- data/lib/mcp/server.rb +192 -23
- 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 +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 829c2bc3c3644f2a7edcf5fd44b96d87887c37da3a4cf4766eed79a2676425ee
|
|
4
|
+
data.tar.gz: 1a168f15a16ad704db47e8c8c2aa7be3f4f869af2a5199fa54d44b4a05990aa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6ca51112faf60a4a2b9f5be2db20f9fe25b6581ca327708f483c03f021b9a68ce8df8c3342e5430132ac670103a26d4ac2f9617e3e0358e074745d6fa4c7743
|
|
7
|
+
data.tar.gz: a57eea28ea7e173422e855702789780381032e888bed42eb42900251e05f8a2806df4220c50c0727423d6c5a8e9124127cdc53e9e9bf34170e6a7c2afa058e7a
|
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,72 @@ response = client.call_tool(
|
|
|
2112
2318
|
|
|
2113
2319
|
The server will send `notifications/progress` back to the client during execution.
|
|
2114
2320
|
|
|
2321
|
+
`MCP::Client::HTTP.new` accepts an optional `max_message_bytes:` keyword that caps the bytes buffered in memory for a single message from the server -
|
|
2322
|
+
an SSE event or a JSON response body. A message that reaches this limit before completing is rejected as a transport error, preventing unbounded memory growth from
|
|
2323
|
+
a server that never terminates an SSE event. It defaults to `4 * 1024 * 1024` (4 MiB); raise it if your server returns larger responses.
|
|
2324
|
+
|
|
2325
|
+
#### Server-to-Client Requests (Elicitation)
|
|
2326
|
+
|
|
2327
|
+
Servers can send requests back to the client while one of the client's own requests is in flight - for example,
|
|
2328
|
+
[`elicitation/create`](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) to ask the user for additional input during a tool call.
|
|
2329
|
+
Register a handler and advertise the capability on `connect` to respond to them:
|
|
2330
|
+
|
|
2331
|
+
```ruby
|
|
2332
|
+
client.connect(capabilities: { elicitation: {} })
|
|
2333
|
+
|
|
2334
|
+
client.on_elicitation do |params|
|
|
2335
|
+
{
|
|
2336
|
+
action: "accept",
|
|
2337
|
+
# Fill fields omitted by the user with the schema's `default` values (SEP-1034)
|
|
2338
|
+
content: MCP::Client::Elicitation.apply_defaults(params["requestedSchema"]),
|
|
2339
|
+
}
|
|
2340
|
+
end
|
|
2341
|
+
```
|
|
2342
|
+
|
|
2343
|
+
Registering a handler opens a standalone HTTP GET SSE stream on a background thread
|
|
2344
|
+
([listening for messages from the server](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server)),
|
|
2345
|
+
since servers deliver requests that are not tied to a client request on that stream. Server requests with no registered handler are answered with
|
|
2346
|
+
a JSON-RPC `-32601` (method not found) error. To handle methods other than `elicitation/create`, register directly on the transport with
|
|
2347
|
+
`http_transport.on_server_request("method/name") { |params| ... }`.
|
|
2348
|
+
|
|
2349
|
+
#### Server-to-Client Requests (Sampling)
|
|
2350
|
+
|
|
2351
|
+
Servers can also request an LLM completion from the client with [`sampling/createMessage`](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling),
|
|
2352
|
+
letting a server leverage the client's model access without its own API keys.
|
|
2353
|
+
|
|
2354
|
+
> MCP Sampling is deprecated as of protocol version `2026-07-28` (SEP-2577), while remaining fully supported under `2025-11-25`.
|
|
2355
|
+
> Register this handler to interoperate with servers that still send sampling requests during the deprecation window;
|
|
2356
|
+
> new servers should call LLM provider APIs directly.
|
|
2357
|
+
|
|
2358
|
+
Register a handler and advertise the capability on `connect`:
|
|
2359
|
+
|
|
2360
|
+
```ruby
|
|
2361
|
+
client.connect(capabilities: { sampling: {} })
|
|
2362
|
+
|
|
2363
|
+
client.on_sampling do |params|
|
|
2364
|
+
completion = my_llm.complete(params["messages"], max_tokens: params["maxTokens"])
|
|
2365
|
+
{
|
|
2366
|
+
role: "assistant",
|
|
2367
|
+
content: { type: "text", text: completion.text },
|
|
2368
|
+
model: completion.model,
|
|
2369
|
+
stopReason: "endTurn",
|
|
2370
|
+
}
|
|
2371
|
+
end
|
|
2372
|
+
```
|
|
2373
|
+
|
|
2374
|
+
For trust and safety, the spec recommends a human in the loop able to review, edit, or reject the request and the generated response.
|
|
2375
|
+
To reject a request, raise `MCP::Client::ServerRequestError` with the spec's user-rejection code `-1`:
|
|
2376
|
+
|
|
2377
|
+
```ruby
|
|
2378
|
+
client.on_sampling do |params|
|
|
2379
|
+
raise MCP::Client::ServerRequestError.new("User rejected sampling request", code: -1) unless approved?(params)
|
|
2380
|
+
|
|
2381
|
+
generate_completion(params)
|
|
2382
|
+
end
|
|
2383
|
+
```
|
|
2384
|
+
|
|
2385
|
+
Use `capabilities: { sampling: { tools: {} } }` to receive tool-enabled sampling requests. Like elicitation, this uses the same standalone GET SSE listening stream.
|
|
2386
|
+
|
|
2115
2387
|
#### HTTP Authorization
|
|
2116
2388
|
|
|
2117
2389
|
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 +2460,17 @@ Required keyword arguments to `Provider.new`:
|
|
|
2188
2460
|
an explicit value always wins.
|
|
2189
2461
|
- `redirect_uri`: String. Must use HTTPS or be a loopback URL (`localhost`, `127.0.0.0/8`, `::1`); other values raise `Provider::InsecureRedirectURIError`.
|
|
2190
2462
|
- `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`.
|
|
2463
|
+
- `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
|
|
2464
|
+
(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
|
|
2465
|
+
the authorization server's issuer, and a missing one is rejected when the server advertises `authorization_response_iss_parameter_supported`.
|
|
2192
2466
|
|
|
2193
2467
|
Optional keyword arguments:
|
|
2194
2468
|
|
|
2195
2469
|
- `scope`: Space-separated scopes to request when the server's `WWW-Authenticate` does not specify one.
|
|
2196
2470
|
- `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.
|
|
2471
|
+
which keeps credentials in process memory only. Persisted `client_information` is stamped with an `"issuer"` member binding it to the authorization server that
|
|
2472
|
+
issued it (SEP-2352): when the server's authorization server changes, the SDK discards the stale registration and its tokens and re-registers automatically
|
|
2473
|
+
(portable CIMD `client_id`s are kept). Treat the hash as opaque and persist it as-is.
|
|
2198
2474
|
- `client_id_metadata_document_url`: URL where you publish a Client ID Metadata Document
|
|
2199
2475
|
(`draft-ietf-oauth-client-id-metadata-document` and the MCP authorization specification).
|
|
2200
2476
|
When the authorization server advertises `client_id_metadata_document_supported: true`,
|
|
@@ -2271,6 +2547,43 @@ Keyword arguments:
|
|
|
2271
2547
|
- `token_endpoint_auth_method`: `"client_secret_basic"` (default) or `"client_secret_post"`. `"none"` is rejected with `ClientCredentialsProvider::InvalidCredentialsError`.
|
|
2272
2548
|
- `scope`, `storage`: Optional, same meaning as on `Provider`.
|
|
2273
2549
|
|
|
2550
|
+
##### Cross-App Access (JWT Bearer) Grant
|
|
2551
|
+
|
|
2552
|
+
For enterprise MCP deployments where an identity provider (IdP) governs authorization (SEP-990), use `MCP::Client::OAuth::CrossAppAccessProvider` instead of `Provider`.
|
|
2553
|
+
The client exchanges an IdP-issued ID token for an Identity Assertion Authorization Grant (ID-JAG) at the IdP via RFC 8693 token exchange, then presents the ID-JAG
|
|
2554
|
+
to the MCP authorization server with the RFC 7523 `jwt-bearer` grant, authenticating with `client_secret_basic`. There is no authorization request, PKCE, DCR, or `offline_access`.
|
|
2555
|
+
Mirrors `CrossAppAccessProvider` and `requestJwtAuthorizationGrant` in the TypeScript SDK.
|
|
2556
|
+
|
|
2557
|
+
`MCP::Client::OAuth::IDJAGTokenExchange.request` performs the RFC 8693 exchange at the IdP token endpoint. Wrap it in a callable so the same provider can plug into
|
|
2558
|
+
an enterprise secret store or a test double without changing the transport wiring.
|
|
2559
|
+
|
|
2560
|
+
```ruby
|
|
2561
|
+
provider = MCP::Client::OAuth::CrossAppAccessProvider.new(
|
|
2562
|
+
client_id: "my-mcp-client",
|
|
2563
|
+
client_secret: ENV.fetch("MCP_CLIENT_SECRET"),
|
|
2564
|
+
assertion_provider: ->(audience:, resource:) {
|
|
2565
|
+
MCP::Client::OAuth::IDJAGTokenExchange.request(
|
|
2566
|
+
token_endpoint: "https://idp.example.com/token",
|
|
2567
|
+
id_token: ENV.fetch("IDP_ID_TOKEN"),
|
|
2568
|
+
client_id: "my-idp-client",
|
|
2569
|
+
audience: audience,
|
|
2570
|
+
resource: resource,
|
|
2571
|
+
)
|
|
2572
|
+
},
|
|
2573
|
+
# scope: "mcp:read mcp:write" (optional; used when neither WWW-Authenticate nor PRM specify one)
|
|
2574
|
+
)
|
|
2575
|
+
|
|
2576
|
+
transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp", oauth: provider)
|
|
2577
|
+
```
|
|
2578
|
+
|
|
2579
|
+
Keyword arguments:
|
|
2580
|
+
|
|
2581
|
+
- `client_id`, `client_secret`: Required. The `jwt-bearer` grant authenticates with `client_secret_basic` at the MCP authorization server.
|
|
2582
|
+
- `assertion_provider`: Required. Callable invoked as `call(audience:, resource:)` and returning the ID-JAG assertion.
|
|
2583
|
+
`audience` is the MCP authorization server's validated issuer identifier; `resource` is the canonical MCP server URL (RFC 8707).
|
|
2584
|
+
Passing both through to `IDJAGTokenExchange.request` covers the common case.
|
|
2585
|
+
- `scope`, `storage`: Optional, same meaning as on `Provider`.
|
|
2586
|
+
|
|
2274
2587
|
##### Communication Security
|
|
2275
2588
|
|
|
2276
2589
|
When `oauth:` is set, the MCP transport URL and every OAuth-facing URL (PRM, Authorization Server metadata, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`,
|
|
@@ -2302,6 +2615,16 @@ The client provides a wrapper class for tools returned by the server:
|
|
|
2302
2615
|
|
|
2303
2616
|
This class provides easy access to tool properties like name, description, input schema, and output schema.
|
|
2304
2617
|
|
|
2618
|
+
### Multi-Round-Trip Results (Experimental, SEP-2322)
|
|
2619
|
+
|
|
2620
|
+
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`,
|
|
2621
|
+
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
|
|
2622
|
+
and an opaque `requestState`; the client fulfills the requests and re-issues the original request with `inputResponses` and the echoed `requestState`.
|
|
2623
|
+
|
|
2624
|
+
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`,
|
|
2625
|
+
and the raw `result`; automatic resumption is not implemented yet, so callers respond manually if they opt into the draft flow. `MCP::ResultType::COMPLETE` and `MCP::ResultType::INPUT_REQUIRED`
|
|
2626
|
+
are provided for forward compatibility. Servers on stable protocol versions never send `resultType`, so existing behavior is unchanged.
|
|
2627
|
+
|
|
2305
2628
|
## Conformance Testing
|
|
2306
2629
|
|
|
2307
2630
|
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
|
@@ -16,7 +16,7 @@ module JsonRpcHandler
|
|
|
16
16
|
PARSE_ERROR = -32700
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
DEFAULT_ALLOWED_ID_CHARACTERS = /\A[a-zA-Z0-9_-]+\z
|
|
19
|
+
DEFAULT_ALLOWED_ID_CHARACTERS = /\A[a-zA-Z0-9_-]+\z/.freeze
|
|
20
20
|
|
|
21
21
|
# Sentinel return value from a handler. When a handler returns this,
|
|
22
22
|
# `process_request` emits no JSON-RPC response for the request,
|
|
@@ -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
|