mcp 1.2.0 → 1.3.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.
data/README.md CHANGED
@@ -2,12 +2,21 @@
2
2
 
3
3
  The official Ruby SDK for Model Context Protocol servers and clients.
4
4
 
5
+ Detailed guides are available at https://ruby.sdk.modelcontextprotocol.io.
6
+
7
+ ## Features
8
+
9
+ - Build [MCP servers](https://ruby.sdk.modelcontextprotocol.io/server/) that expose tools, prompts, and resources to any MCP host
10
+ - Build [MCP clients](https://ruby.sdk.modelcontextprotocol.io/client/) that connect to any MCP server, with automatic lifecycle negotiation and OAuth 2.1 authorization
11
+ - Speak every standard transport: stdio and Streamable HTTP (including SSE), with a Rails integration
12
+ - Cover the full protocol surface: server-to-client requests, multi round-trip results, notifications, progress, logging, cancellation, completions, and pagination
13
+
5
14
  ## Installation
6
15
 
7
16
  Add this line to your application's Gemfile:
8
17
 
9
18
  ```ruby
10
- gem 'mcp'
19
+ gem "mcp"
11
20
  ```
12
21
 
13
22
  And then execute:
@@ -24,81 +33,14 @@ $ gem install mcp
24
33
 
25
34
  You may need to add additional dependencies depending on which features you wish to access.
26
35
 
27
- ## Building an MCP Server
28
-
29
- The `MCP::Server` class is the core component that handles JSON-RPC requests and responses.
30
- It implements the Model Context Protocol specification, handling model context requests and responses.
31
-
32
- ### Key Features
33
-
34
- - Implements JSON-RPC 2.0 message handling
35
- - Supports protocol initialization and capability negotiation
36
- - Manages tool registration and invocation
37
- - Supports prompt registration and execution
38
- - Supports resource registration and retrieval
39
- - Supports stdio & Streamable HTTP (including SSE) transports
40
- - Supports notifications for list changes (tools, prompts, resources)
41
- - Supports roots (server-to-client filesystem boundary queries)
42
- - Supports sampling (server-to-client LLM completion requests)
43
- - Supports cursor-based pagination for list operations
44
- - Supports cancellation of in-flight requests on both server and client (notifications/cancelled)
45
-
46
- ### Supported Methods
47
-
48
- - `initialize` - Initializes the protocol and returns server capabilities
49
- - `server/discover` - Sessionless capability discovery (MCP 2026-07-28, SEP-2575): returns the modern `supportedVersions`,
50
- `capabilities`, `instructions`, the required `ttlMs`/`cacheScope` cache hints, and the server identity as the optional
51
- `io.modelcontextprotocol/serverInfo` stamp in the result `_meta`, and responds before `initialize`
52
- and without an `Mcp-Session-Id`. The server also serves the full stateless modern lifecycle: requests carrying the SEP-2575 `_meta` envelope
53
- (`io.modelcontextprotocol/protocolVersion`, `clientInfo`, and `clientCapabilities`) are validated per request,
54
- and the Streamable HTTP transport serves them on a sessionless single-exchange path. On the client, `MCP::Client#connect` negotiates
55
- the lifecycle automatically by default (probe `server/discover`, fall back to the `initialize` handshake), `connect(mode: :modern)` skips
56
- the handshake entirely, `connect(mode: :legacy)` forces the classic handshake, and `MCP::Client#discover` exposes the raw discovery result
57
- - `subscriptions/listen` - Long-lived notification subscription stream (MCP 2026-07-28, SEP-2575), replacing the legacy HTTP GET listening stream:
58
- the client opts in via the `notifications` filter (`toolsListChanged` / `promptsListChanged` / `resourcesListChanged` / `resourceSubscriptions`),
59
- the server acknowledges the honored subset with `notifications/subscriptions/acknowledged` as the first stream message,
60
- and every delivered notification carries the correlating `io.modelcontextprotocol/subscriptionId` in `_meta`. Served on the Streamable HTTP modern path;
61
- stdio answers `-32601`. Concurrent streams are capped by `max_listen_subscriptions:` (default 1000), and each stream receives an SSE keepalive
62
- comment frame every `listen_keepalive_interval:` seconds (default 15) so a dropped connection frees its slot; pass `listen_keepalive_interval: nil`
63
- when an upstream proxy already keeps the stream alive
64
- - Multi round-trip `input_required` results (MCP 2026-07-28, SEP-2322): a `tools/call`, `prompts/get`, or `resources/read` handler that
65
- opts in to `server_context:` may return `MCP::Server::InputRequiredResult.new(input_requests:, request_state:)` to ask the client for
66
- additional input (`elicitation/create`, `sampling/createMessage`, or `roots/list` shapes) instead of performing a server-initiated request,
67
- which the modern lifecycle forbids. On the retried request the handler re-runs from the start and reads the answers via
68
- `server_context.input_responses` / `server_context.input_response(key)` and the echoed opaque `server_context.request_state`
69
- (deterministic replay; the server holds no memory between rounds). The SDK rejects issuance on legacy requests and returns `-32021`
70
- when an embedded request needs a client capability the request did not declare. The echoed `requestState` arrives as
71
- client-controlled input: pass `MCP::Server::RequestStateSecurity.new(key:)` (a 32-byte key) via `Server.new(request_state_security:)` to
72
- have it sealed with AES-256-GCM and bound to a TTL plus the originating method, target, and arguments, all transparently to handlers.
73
- Multi-process deployments must share the key across workers; without `request_state_security:` the state crosses the wire exactly as
74
- the handler wrote it and protecting it is the handler author's responsibility. On the client, register handlers with
75
- `on_elicitation` / `on_sampling` / `on_roots` - the same registrations that answer a real server-to-client request - and
76
- declare the matching capabilities on `connect` (a server embeds only the request kinds the client declared);
77
- `call_tool` / `get_prompt` / `read_resource` then resume `input_required` results automatically: each embedded request is fulfilled by
78
- the matching handler and the original request is re-issued with `inputResponses` plus the echoed `requestState`
79
- (with exponential backoff for `requestState`-only load-shedding legs). Without a matching handler they raise `MCP::Client::InputRequiredError`,
80
- and the `input_responses:` / `request_state:` keyword arguments support manual driving
81
- - `ping` - Simple health check
82
- - `logging/setLevel` - Configures the minimum log level for the server
83
- - `tools/list` - Lists all registered tools and their schemas
84
- - `tools/call` - Invokes a specific tool with provided arguments
85
- - `prompts/list` - Lists all registered prompts and their schemas
86
- - `prompts/get` - Retrieves a specific prompt by name
87
- - `resources/list` - Lists all registered resources and their schemas
88
- - `resources/read` - Retrieves a specific resource by name
89
- - `resources/templates/list` - Lists all registered resource templates and their schemas
90
- - `resources/subscribe` - Subscribes to updates for a specific resource
91
- - `resources/unsubscribe` - Unsubscribes from updates for a specific resource
92
- - `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
93
- - `roots/list` - Requests filesystem roots from the client (server-to-client)
94
- - `sampling/createMessage` - Requests LLM completion from the client (server-to-client)
95
- - `elicitation/create` - Requests user input from the client (server-to-client)
36
+ ## Quick Start
96
37
 
97
- ### Usage
38
+ The following minimal programs show both sides of the protocol: a server that exposes a single tool,
39
+ and a client that spawns such a server and drives it over stdio.
98
40
 
99
- #### Stdio Transport
41
+ ### MCP Server
100
42
 
101
- If you want to build a local command-line application, you can use the stdio transport:
43
+ A minimal server defines a tool and serves it over the stdio transport:
102
44
 
103
45
  ```ruby
104
46
  require "mcp"
@@ -134,2923 +76,64 @@ transport = MCP::Server::Transports::StdioTransport.new(server)
134
76
  transport.open
135
77
  ```
136
78
 
137
- `StdioTransport.new` accepts an optional `max_line_bytes:` keyword that caps the byte length of a single newline-delimited request frame. A frame that reaches this limit without a newline is rejected and the connection is closed, preventing unbounded memory growth from a peer that never emits a newline. It defaults to `4 * 1024 * 1024` (4 MiB).
138
-
139
- You can run this script and then type in requests to the server at the command line.
79
+ Save the script as `server.rb`, run it, and send JSON-RPC requests via stdin:
140
80
 
141
81
  ```console
142
- $ ruby examples/stdio_server.rb
82
+ $ ruby server.rb
143
83
  {"jsonrpc":"2.0","id":"1","method":"ping"}
144
84
  {"jsonrpc":"2.0","id":"2","method":"tools/list"}
145
85
  {"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
146
86
  ```
147
87
 
148
- #### Streamable HTTP Transport
149
-
150
- `MCP::Server::Transports::StreamableHTTPTransport` is a standard Rack app, so it can be mounted in any Rack-compatible framework.
151
- The following examples show two common integration styles in Rails.
152
-
153
- > [!IMPORTANT]
154
- > `MCP::Server::Transports::StreamableHTTPTransport` stores session and SSE stream state in memory,
155
- > so it must run in a single process. Use a single-process server (e.g., Puma with `workers 0`).
156
- > Multi-process configurations (Unicorn, or Puma with `workers > 0`) fork separate processes that
157
- > do not share memory, which breaks session management and SSE connections.
158
- >
159
- > When running multiple server instances behind a load balancer, configure your load balancer to use
160
- > sticky sessions (session affinity) so that requests with the same `Mcp-Session-Id` header are always
161
- > routed to the same instance.
162
- >
163
- > Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
164
-
165
- > [!IMPORTANT]
166
- > Per MCP 2025-11-25, `StreamableHTTPTransport` validates the `Host` and `Origin` headers by default to
167
- > prevent DNS rebinding attacks against locally bound servers, rejecting unauthorized values with HTTP 403.
168
- > `Host` is allowed for the loopback defaults (`127.0.0.1`, `::1`, `localhost`), and an `Origin` header,
169
- > when present, must be same-origin or explicitly allow-listed. Non-browser clients that send no `Origin`
170
- > header are unaffected.
171
- >
172
- > Deployments behind a reverse proxy or bound to a non-loopback interface must widen the allow lists:
173
- >
174
- > ```ruby
175
- > transport = MCP::Server::Transports::StreamableHTTPTransport.new(
176
- > server,
177
- > allowed_hosts: ["mcp.example.com"],
178
- > allowed_origins: ["https://app.example.com"],
179
- > )
180
- > ```
181
- >
182
- > An `allowed_hosts:` entry matches either the bare host name (any port) or the full `host:port` value,
183
- > so both `"mcp.example.com"` and `"mcp.example.com:8443"` work. Pass `dns_rebinding_protection: false`
184
- > to disable the check entirely (e.g., when an upstream proxy or middleware already validates `Host`/`Origin`).
185
-
186
- ##### Rails (mount)
187
-
188
- `StreamableHTTPTransport` is a Rack app that can be mounted directly in Rails routes:
189
-
190
- ```ruby
191
- # config/routes.rb
192
- server = MCP::Server.new(
193
- name: "my_server",
194
- title: "Example Server Display Name",
195
- version: "1.0.0",
196
- instructions: "Use the tools of this server as a last resort",
197
- tools: [SomeTool, AnotherTool],
198
- prompts: [MyPrompt],
199
- )
200
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
201
-
202
- Rails.application.routes.draw do
203
- mount transport => "/mcp"
204
- end
205
- ```
206
-
207
- `mount` directs all HTTP methods on `/mcp` to the transport. `StreamableHTTPTransport` internally dispatches
208
- `POST` (client-to-server JSON-RPC messages, with responses optionally streamed via SSE),
209
- `GET` (optional standalone SSE stream for server-to-client messages), and `DELETE` (session termination) per
210
- the [MCP Streamable HTTP transport spec](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http),
211
- so no additional route configuration is needed.
212
-
213
- A complete runnable application using this approach is available in [`examples/rails`](examples/rails).
214
-
215
- ##### Rails (controller)
216
-
217
- While the mount approach creates a single server at boot time, the controller approach creates a new server per request.
218
- This allows you to customize tools, prompts, or configuration based on the request (e.g., different tools per route).
219
-
220
- `StreamableHTTPTransport#handle_request` returns proper HTTP status codes (e.g., 202 Accepted for notifications):
221
-
222
- ```ruby
223
- class McpController < ActionController::API
224
- def create
225
- server = MCP::Server.new(
226
- name: "my_server",
227
- title: "Example Server Display Name",
228
- version: "1.0.0",
229
- instructions: "Use the tools of this server as a last resort",
230
- tools: [SomeTool, AnotherTool],
231
- prompts: [MyPrompt],
232
- server_context: { user_id: current_user.id },
233
- )
234
- # Since the `MCP-Session-Id` is not shared across requests, `stateless: true` is set.
235
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
236
- status, headers, body = transport.handle_request(request)
237
-
238
- render(json: body.first, status: status, headers: headers)
239
- end
240
- end
241
- ```
242
-
243
- ### Configuration
244
-
245
- The gem can be configured using the `MCP.configure` block:
246
-
247
- ```ruby
248
- MCP.configure do |config|
249
- config.exception_reporter = ->(exception, server_context) {
250
- # Your exception reporting logic here
251
- # For example with Bugsnag:
252
- Bugsnag.notify(exception) do |report|
253
- report.add_metadata(:model_context_protocol, server_context)
254
- end
255
- }
256
-
257
- config.around_request = ->(data, &request_handler) {
258
- logger.info("Start: #{data[:method]}")
259
- request_handler.call
260
- logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
261
- }
262
- end
263
- ```
264
-
265
- or by creating an explicit configuration and passing it into the server.
266
- This is useful for systems where an application hosts more than one MCP server but
267
- they might require different configurations.
268
-
269
- ```ruby
270
- configuration = MCP::Configuration.new
271
- configuration.exception_reporter = ->(exception, server_context) {
272
- # Your exception reporting logic here
273
- # For example with Bugsnag:
274
- Bugsnag.notify(exception) do |report|
275
- report.add_metadata(:model_context_protocol, server_context)
276
- end
277
- }
278
-
279
- configuration.around_request = ->(data, &request_handler) {
280
- logger.info("Start: #{data[:method]}")
281
- request_handler.call
282
- logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
283
- }
284
-
285
- server = MCP::Server.new(
286
- # ... all other options
287
- configuration:,
288
- )
289
- ```
290
-
291
- ### Capability Extensions
292
-
293
- Per SEP-2133, both clients and servers can declare protocol extensions under the `extensions` member of their capabilities.
294
- Keys are extension identifiers using the reverse-DNS prefix convention (e.g. `"io.modelcontextprotocol/tasks"`, `"com.example/feature"`);
295
- values are extension-defined configuration objects, with `{}` meaning "supported with no settings".
296
-
297
- On the server, declare extensions through the `capabilities` keyword, either as a plain hash or via the `MCP::Server::Capabilities` builder:
298
-
299
- ```ruby
300
- capabilities = MCP::Server::Capabilities.new
301
- capabilities.support_tools
302
- capabilities.support_extensions("com.example/feature" => { enabled: true })
303
-
304
- server = MCP::Server.new(name: "my_server", capabilities: capabilities)
305
- ```
306
-
307
- The declared extensions appear in the `initialize` result's `capabilities.extensions`. Extensions the client declared during `initialize` are
308
- readable via `server.client_capabilities[:extensions]` (or `session.client_capabilities[:extensions]` for per-session transports).
309
-
310
- On the client, pass extensions through `connect`:
311
-
312
- ```ruby
313
- client.connect(capabilities: { extensions: { "com.example/feature" => {} } })
314
- ```
88
+ The same server can also run over Streamable HTTP, including mounted inside a Rails application;
89
+ see [Server Transports](https://ruby.sdk.modelcontextprotocol.io/server/transports/).
315
90
 
316
- ### MCP Apps (SEP-1865)
91
+ ### MCP Client
317
92
 
318
- MCP Apps is a Final extension (negotiated via the Capability Extensions mechanism above) that lets a server ship interactive
319
- HTML user interfaces which the host renders for tool results. On the server side the extension is a thin convention,
320
- and `MCP::Apps` provides the vocabulary and helpers:
93
+ A minimal client spawns a stdio server as a subprocess, connects, and lists and calls its tools:
321
94
 
322
95
  ```ruby
323
- capabilities = MCP::Server::Capabilities.new
324
- capabilities.support_tools
325
- capabilities.support_resources
326
- capabilities.support_extensions(MCP::Apps.capability) # { "io.modelcontextprotocol/ui" => { mimeTypes: [...] } }
327
-
328
- server = MCP::Server.new(
329
- name: "weather_server",
330
- capabilities: capabilities,
331
- # UI templates are ordinary resources with a `ui://` URI and the `text/html;profile=mcp-app` MIME type.
332
- resources: [MCP::Apps.ui_resource(uri: "ui://weather-server/dashboard", name: "weather_dashboard")],
96
+ stdio_transport = MCP::Client::Stdio.new(
97
+ command: "bundle",
98
+ args: ["exec", "ruby", "path/to/server.rb"],
99
+ env: { "API_KEY" => "my_secret_key" },
100
+ read_timeout: 30
333
101
  )
102
+ client = MCP::Client.new(transport: stdio_transport)
334
103
 
335
- server.resources_read_handler do |params|
336
- [{ uri: params[:uri], mimeType: MCP::Apps::RESOURCE_MIME_TYPE, text: "<html>...</html>" }]
337
- end
104
+ # Perform the MCP initialization handshake before sending any requests.
105
+ client.connect
338
106
 
339
- # Link the tool to its template via `_meta.ui.resourceUri` (pass `legacy: true` to also
340
- # emit the older flat `"ui/resourceUri"` alias for hosts that predate the Final spec).
341
- server.define_tool(
342
- name: "get_weather",
343
- meta: MCP::Apps.tool_meta(resource_uri: "ui://weather-server/dashboard"),
344
- ) do |server_context:|
345
- # The extension is optional: always return a meaningful text result, and use
346
- # `MCP::Apps.client_supports?` when UI-capable clients should get richer structured content.
347
- MCP::Apps.client_supports?(server.client_capabilities) # => true when the host declared the extension
348
- MCP::Tool::Response.new([{ type: "text", text: "Sunny, 22 degrees Celsius" }])
107
+ # List available tools.
108
+ tools = client.tools
109
+ tools.each do |tool|
110
+ puts "Tool: #{tool.name} - #{tool.description}"
349
111
  end
350
- ```
351
-
352
- Everything else the extension defines (the sandboxed iframe, the `ui/*` postMessage bridge, consent for UI-initiated actions)
353
- is the HOST's responsibility; a server only ever receives ordinary `resources/read` and `tools/call` requests.
354
- See the [MCP Apps specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx).
355
-
356
- ### Server Context and Configuration Block Data
357
-
358
- #### `server_context`
359
112
 
360
- The `server_context` is a user-defined hash that is passed into the server instance and made available to tool and prompt calls.
361
- It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
362
-
363
- **Type:**
364
-
365
- ```ruby
366
- server_context: { [String, Symbol] => Any }
367
- ```
368
-
369
- **Example:**
370
-
371
- ```ruby
372
- server = MCP::Server.new(
373
- name: "my_server",
374
- server_context: { user_id: current_user.id, request_id: request.uuid }
113
+ # Call a specific tool.
114
+ response = client.call_tool(
115
+ tool: tools.first,
116
+ arguments: { message: "Hello, world!" }
375
117
  )
376
- ```
377
-
378
- This hash is then passed as the `server_context` keyword argument to tool and prompt calls.
379
- Note that the exception reporter does not receive this user-defined hash, and instrumentation
380
- callbacks omit it unless you opt in with `instrument_server_context`.
381
- See the relevant sections below for the arguments they receive.
382
-
383
- #### Request-specific `_meta` Parameter
384
-
385
- The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/2025-06-18/basic#general-fields) in requests that allows clients to pass request-specific metadata. The server automatically extracts this parameter and makes it available to tools and prompts as a nested field within the `server_context`.
386
-
387
- > [!NOTE]
388
- > `_meta` is only merged when `server_context` is a `Hash` (or `nil`, in which case a new `{ _meta: ... }` hash is synthesized).
389
- > If you assign a non-`Hash` value to `server_context`, `_meta` is not merged and tools will not see it
390
- > under `server_context[:_meta]`. Keep `server_context` as a `Hash` if your tools need access to `_meta`.
391
-
392
- **Access Pattern:**
393
-
394
- When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
395
-
396
- ```ruby
397
- class MyTool < MCP::Tool
398
- def self.call(message:, server_context:)
399
- # Access provider-specific metadata
400
- session_id = server_context.dig(:_meta, :session_id)
401
- request_id = server_context.dig(:_meta, :request_id)
402
-
403
- # Access server's original context
404
- user_id = server_context.dig(:user_id)
405
-
406
- MCP::Tool::Response.new([{
407
- type: "text",
408
- text: "Processing for user #{user_id} in session #{session_id}"
409
- }])
410
- end
411
- end
412
- ```
413
-
414
- **Client Request Example:**
415
-
416
- ```json
417
- {
418
- "jsonrpc": "2.0",
419
- "id": 1,
420
- "method": "tools/call",
421
- "params": {
422
- "name": "my_tool",
423
- "arguments": { "message": "Hello" },
424
- "_meta": {
425
- "session_id": "abc123",
426
- "request_id": "req_456"
427
- }
428
- }
429
- }
430
- ```
431
-
432
- **Distributed Tracing (W3C Trace Context):**
433
-
434
- Per SEP-414, the keys `traceparent`, `tracestate`, and `baggage` are reserved un-prefixed `_meta` keys for propagating
435
- [W3C Trace Context](https://www.w3.org/TR/trace-context/) across MCP requests. The SDK guarantees these keys pass through
436
- incoming request `_meta` untouched, and exposes their names as constants on `MCP::TraceContext` (`TRACEPARENT_META_KEY`,
437
- `TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`, and `META_KEYS`). The SDK does not depend on OpenTelemetry; bridge the values
438
- to your tracing system yourself:
439
-
440
- ```ruby
441
- class TracedTool < MCP::Tool
442
- def self.call(message:, server_context:)
443
- traceparent = server_context.dig(:_meta, :traceparent)
444
- # Hand traceparent/tracestate/baggage to your tracing library
445
- # (e.g. the opentelemetry-ruby gems) to continue the caller's trace.
446
-
447
- MCP::Tool::Response.new([{ type: "text", text: "ok" }])
448
- end
449
- end
450
- ```
451
-
452
- On the client side, every request method (`call_tool`, `read_resource`, `get_prompt`, `complete`, `ping`, and the `list_*` methods)
453
- accepts a `meta:` keyword to inject these keys into the outgoing request, so trace context can flow on every request:
454
-
455
- ```ruby
456
- meta = { MCP::TraceContext::TRACEPARENT_META_KEY => "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" }
457
-
458
- client.call_tool(tool: tool, arguments: { message: "Hello" }, meta: meta)
459
- client.read_resource(uri: "file:///report.txt", meta: meta)
460
- ```
461
-
462
- #### Configuration Block Data
463
-
464
- ##### Exception Reporter
465
-
466
- The exception reporter receives:
467
-
468
- - `exception`: The Ruby exception object that was raised
469
- - `server_context`: A hash describing where the failure occurred (e.g., `{ request: <raw JSON-RPC request> }`
470
- for request handling, `{ notification: "tools_list_changed" }` for notification delivery).
471
- This is not the user-defined `server_context` passed to `Server.new`.
472
-
473
- **Signature:**
474
-
475
- ```ruby
476
- exception_reporter = ->(exception, server_context) { ... }
477
- ```
478
-
479
- ##### Around Request
480
-
481
- The `around_request` hook wraps request handling, allowing you to execute code before and after each request.
482
- This is useful for Application Performance Monitoring (APM) tracing, logging, or other observability needs.
483
-
484
- The hook receives a `data` hash and a `request_handler` block. You must call `request_handler.call` to execute the request:
485
-
486
- **Signature:**
487
-
488
- ```ruby
489
- around_request = ->(data, &request_handler) { request_handler.call }
490
- ```
491
-
492
- **`data` availability by timing:**
493
-
494
- - Before `request_handler.call`: `method`, and `server_context` when `instrument_server_context` is enabled
495
- - After `request_handler.call`: `tool_name`, `tool_arguments`, `prompt_name`, `resource_uri`, `error`, `client`
496
- - Not available inside `around_request`: `duration` (added after `around_request` returns)
497
-
498
- **Exposing the user-defined `server_context` (opt in):**
499
-
500
- `data` omits the user-defined `server_context` by default, because that hash is
501
- application-supplied and may hold values a tracing backend should not receive.
502
- Enable it when you need to tag spans with the request's subject:
503
-
504
- ```ruby
505
- MCP.configure do |config|
506
- config.instrument_server_context = true
507
-
508
- config.around_request = ->(data, &request_handler) {
509
- Sentry.set_user(id: data.dig(:server_context, :user_id))
510
- request_handler.call
511
- }
512
- end
513
- ```
514
-
515
- `data[:server_context]` is the hash passed to `Server.new` — `nil` when the host
516
- set none. It is not the exception reporter's context argument, which describes
517
- where a failure occurred rather than who made the request.
518
-
519
- > [!NOTE]
520
- > `tool_name`, `prompt_name` and `resource_uri` may only be populated for the corresponding request methods
521
- > (`tools/call`, `prompts/get`, `resources/read`), and may not be set depending on how the request is handled
522
- > (for example, `prompt_name` is not recorded when the prompt is not found).
523
- > `duration` is added after `around_request` returns, so it is not visible from within the hook.
524
-
525
- **Example:**
526
-
527
- ```ruby
528
- MCP.configure do |config|
529
- config.around_request = ->(data, &request_handler) {
530
- logger.info("Start: #{data[:method]}")
531
- request_handler.call
532
- logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
533
- }
534
- end
535
- ```
536
-
537
- ##### Instrumentation Callback (soft-deprecated)
538
-
539
- > [!NOTE]
540
- > `instrumentation_callback` is soft-deprecated. Use `around_request` instead.
541
- >
542
- > To migrate, wrap the call in `begin/ensure` so the callback still runs when the request fails:
543
- >
544
- > ```ruby
545
- > # Before
546
- > config.instrumentation_callback = ->(data) { log(data) }
547
- >
548
- > # After
549
- > config.around_request = ->(data, &request_handler) do
550
- > request_handler.call
551
- > ensure
552
- > log(data)
553
- > end
554
- > ```
555
- >
556
- > Note that `data[:duration]` is not available inside `around_request`.
557
- > If you need it, measure elapsed time yourself within the hook, or keep using `instrumentation_callback`.
558
-
559
- The instrumentation callback is called after each request finishes, whether successfully or with an error.
560
- It receives a hash with the following possible keys:
561
-
562
- - `method`: (String) The protocol method called (e.g., "ping", "tools/list")
563
- - `tool_name`: (String, optional) The name of the tool called
564
- - `tool_arguments`: (Hash, optional) The arguments passed to the tool
565
- - `prompt_name`: (String, optional) The name of the prompt called
566
- - `resource_uri`: (String, optional) The URI of the resource called
567
- - `error`: (String, optional) Error code if a lookup failed
568
- - `duration`: (Float) Duration of the call in seconds
569
- - `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
570
- - `server_context`: (Any, optional) The user-defined hash passed to `Server.new`, present only when
571
- `instrument_server_context` is enabled
572
-
573
- **Signature:**
574
-
575
- ```ruby
576
- instrumentation_callback = ->(data) { ... }
577
- ```
578
-
579
- ### Server Protocol Version
580
-
581
- The server's protocol version can be overridden using the `protocol_version` keyword argument:
582
-
583
- ```ruby
584
- configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
585
- MCP::Server.new(name: "test_server", configuration: configuration)
586
- ```
587
-
588
- If no protocol version is specified, the latest handshake version (`2025-11-25`) is applied by default.
589
-
590
- This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:
591
-
592
- ```ruby
593
- MCP::Configuration.new(protocol_version: nil)
594
- ```
595
-
596
- If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
597
-
598
- The pin scopes the `initialize` handshake, so it accepts handshake versions (`2025-11-25` and earlier) only. Per the SEP-2575 era model,
599
- `2026-07-28` carries its version on every request and has no handshake at all, so there is nothing for a pin to configure there and setting it raises `ArgumentError`;
600
- a client asking `initialize` for a modern version is counter-offered the pinned version (or the latest handshake version), matching the TypeScript and Python SDKs.
601
- Clients reach `2026-07-28` through `server/discover` and the per-request `_meta` envelope, which the bundled transports serve alongside the handshake with no configuration needed.
602
-
603
- Be sure to check the [MCP spec](https://modelcontextprotocol.io/specification/versioning) for the protocol version to understand the supported features for the version being set.
604
-
605
- ### Exception Reporting
606
-
607
- The exception reporter receives two arguments:
608
-
609
- - `exception`: The Ruby exception object that was raised
610
- - `server_context`: A hash containing contextual information about where the error occurred
611
-
612
- The `server_context` hash includes:
613
-
614
- - For request handling failures: `{ request: { ... } }` (the raw JSON-RPC request hash)
615
- - For notification delivery failures: `{ notification: "tools_list_changed" }` (or the relevant notification name)
616
-
617
- When an exception occurs:
618
-
619
- 1. The exception is reported via the configured reporter
620
- 2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
621
- 3. For other requests, the exception is re-raised after reporting
622
-
623
- If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
624
-
625
- ### Tools
626
-
627
- MCP spec includes [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) which provide functionality to LLM apps.
628
-
629
- This gem provides a `MCP::Tool` class that can be used to create tools in three ways:
630
-
631
- 1. As a class definition:
632
-
633
- ```ruby
634
- class MyTool < MCP::Tool
635
- title "My Tool"
636
- description "This tool performs specific functionality..."
637
- input_schema(
638
- properties: {
639
- message: { type: "string" },
640
- },
641
- required: ["message"]
642
- )
643
- output_schema(
644
- properties: {
645
- result: { type: "string" },
646
- success: { type: "boolean" },
647
- timestamp: { type: "string", format: "date-time" }
648
- },
649
- required: ["result", "success", "timestamp"]
650
- )
651
- annotations(
652
- read_only_hint: true,
653
- destructive_hint: false,
654
- idempotent_hint: true,
655
- open_world_hint: false,
656
- title: "My Tool"
657
- )
658
-
659
- def self.call(message:, server_context:)
660
- MCP::Tool::Response.new([{ type: "text", text: "OK" }])
661
- end
662
- end
663
-
664
- tool = MyTool
665
- ```
666
-
667
- 2. By using the `MCP::Tool.define` method with a block:
668
-
669
- ```ruby
670
- tool = MCP::Tool.define(
671
- name: "my_tool",
672
- title: "My Tool",
673
- description: "This tool performs specific functionality...",
674
- annotations: {
675
- read_only_hint: true,
676
- title: "My Tool"
677
- }
678
- ) do |args, server_context:|
679
- MCP::Tool::Response.new([{ type: "text", text: "OK" }])
680
- end
681
- ```
682
118
 
683
- 3. By using the `MCP::Server#define_tool` method with a block:
684
-
685
- ```ruby
686
- server = MCP::Server.new
687
- server.define_tool(
688
- name: "my_tool",
689
- description: "This tool performs specific functionality...",
690
- annotations: {
691
- title: "My Tool",
692
- read_only_hint: true
693
- }
694
- ) do |args, server_context:|
695
- Tool::Response.new([{ type: "text", text: "OK" }])
696
- end
119
+ # Close the transport when done.
120
+ stdio_transport.close
697
121
  ```
698
122
 
699
- The server_context parameter is the server_context passed into the server and can be used to pass per request information,
700
- e.g. around authentication state.
701
-
702
- Tool arguments arrive as a `Hash` with symbol keys at every nesting level, because the transports parse JSON with `symbolize_names: true`.
703
- Read nested objects with symbol keys (`payload[:subject]`, not `payload["subject"]`).
704
- See [Tool argument keys](docs/building-servers.md#tool-argument-keys) for details and a testing tip.
705
-
706
- ### Tool Annotations
707
-
708
- Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
709
-
710
- - `destructive_hint`: Indicates if the tool performs destructive operations. Defaults to true
711
- - `idempotent_hint`: Indicates if the tool's operations are idempotent. Defaults to false
712
- - `open_world_hint`: Indicates if the tool operates in an open world context. Defaults to true
713
- - `read_only_hint`: Indicates if the tool only reads data (doesn't modify state). Defaults to false
714
- - `title`: A human-readable title for the tool
715
-
716
- Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
717
-
718
- > [!NOTE]
719
- > This **Tool Annotations** feature is supported starting from `protocol_version: '2025-03-26'`.
720
-
721
- ### Tool Output Schemas
722
-
723
- Tools can optionally define an `output_schema` to specify the expected structure of their results. This works similarly to how `input_schema` is defined and can be used in three ways:
724
-
725
- 1. **Class definition with output_schema:**
726
-
727
- ```ruby
728
- class WeatherTool < MCP::Tool
729
- tool_name "get_weather"
730
- description "Get current weather for a location"
731
-
732
- input_schema(
733
- properties: {
734
- location: { type: "string" },
735
- units: { type: "string", enum: ["celsius", "fahrenheit"] }
736
- },
737
- required: ["location"]
738
- )
739
-
740
- output_schema(
741
- properties: {
742
- temperature: { type: "number" },
743
- condition: { type: "string" },
744
- humidity: { type: "integer" }
745
- },
746
- required: ["temperature", "condition", "humidity"]
747
- )
748
-
749
- def self.call(location:, units: "celsius", server_context:)
750
- # Call weather API and structure the response
751
- api_response = WeatherAPI.fetch(location, units)
752
- weather_data = {
753
- temperature: api_response.temp,
754
- condition: api_response.description,
755
- humidity: api_response.humidity_percent
756
- }
757
-
758
- output_schema.validate_result(weather_data)
759
-
760
- MCP::Tool::Response.new([{
761
- type: "text",
762
- text: weather_data.to_json
763
- }])
764
- end
765
- end
766
- ```
123
+ The same client can connect to Streamable HTTP servers with `MCP::Client::HTTP`;
124
+ see [Client Transports](https://ruby.sdk.modelcontextprotocol.io/client/transports/).
767
125
 
768
- 2. **Using Tool.define with output_schema:**
126
+ ## Examples
769
127
 
770
- ```ruby
771
- tool = MCP::Tool.define(
772
- name: "calculate_stats",
773
- description: "Calculate statistics for a dataset",
774
- input_schema: {
775
- properties: {
776
- numbers: { type: "array", items: { type: "number" } }
777
- },
778
- required: ["numbers"]
779
- },
780
- output_schema: {
781
- properties: {
782
- mean: { type: "number" },
783
- median: { type: "number" },
784
- count: { type: "integer" }
785
- },
786
- required: ["mean", "median", "count"]
787
- }
788
- ) do |args, server_context:|
789
- # Calculate statistics and validate against schema
790
- MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
791
- end
792
- ```
128
+ Runnable examples are available in [`examples/`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples),
129
+ including a complete Rails application in [`examples/rails`](https://github.com/modelcontextprotocol/ruby-sdk/tree/main/examples/rails).
793
130
 
794
- 3. **Using OutputSchema objects:**
131
+ ## Documentation
795
132
 
796
- ```ruby
797
- class DataTool < MCP::Tool
798
- output_schema MCP::Tool::OutputSchema.new(
799
- properties: {
800
- success: { type: "boolean" },
801
- data: { type: "object" }
802
- },
803
- required: ["success"]
804
- )
805
- end
806
- ```
133
+ - [SDK guides](https://ruby.sdk.modelcontextprotocol.io)
134
+ - [SDK API documentation](https://rubydoc.info/gems/mcp)
135
+ - [Model Context Protocol documentation](https://modelcontextprotocol.io)
807
136
 
808
- Output schema may also describe an array of objects:
137
+ ## License
809
138
 
810
- ```ruby
811
- class WeatherTool < MCP::Tool
812
- output_schema(
813
- type: "array",
814
- items: {
815
- properties: {
816
- temperature: { type: "number" },
817
- condition: { type: "string" },
818
- humidity: { type: "integer" }
819
- },
820
- required: ["temperature", "condition", "humidity"]
821
- }
822
- )
823
- end
824
- ```
825
-
826
- Please note: in this case, you must provide `type: "array"`. The default type for output schemas is `object`,
827
- applied only when the schema declares no root keyword (`type`, `$ref`, `oneOf`, `anyOf`, `allOf`, `not`, `if`, `const`, `enum`).
828
-
829
- Per SEP-2106, an output schema may be any valid JSON Schema 2020-12 document, including a primitive root
830
- (`{ type: "string" }`) or a root-level composition:
831
-
832
- ```ruby
833
- class FlexibleTool < MCP::Tool
834
- output_schema(
835
- oneOf: [
836
- { type: "string" },
837
- { type: "array", items: { type: "number" } }
838
- ]
839
- )
840
- end
841
- ```
842
-
843
- Input schemas keep `type: "object"` at the root but accept the full 2020-12 vocabulary below it
844
- (`$defs`/`$ref`, `oneOf`/`anyOf`/`allOf`/`not`, `if`/`then`/`else`). Two resource bounds apply to
845
- all tool schemas: only same-document `$ref`s (starting with `#`) are accepted, and documents are
846
- capped at `MCP::Tool::Schema::MAX_SCHEMA_DEPTH` nesting levels and `MCP::Tool::Schema::MAX_SUBSCHEMA_COUNT` subschema objects;
847
- violations raise `ArgumentError` at construction time.
848
-
849
- MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/latest/server/tools#output-schema) specifies that:
850
-
851
- - **Server Validation**: Servers MUST provide structured results that conform to the output schema
852
- - **Client Validation**: Clients SHOULD validate structured results against the output schema
853
- - **Better Integration**: Enables strict schema validation, type information, and improved developer experience
854
- - **Backward Compatibility**: Tools returning structured content SHOULD also include serialized JSON in a TextContent block
855
-
856
- The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
857
-
858
- By default, server-side validation of tool results against `output_schema` is disabled for backwards compatibility. To validate successful tool responses, enable `validate_tool_call_results`:
859
-
860
- ```ruby
861
- configuration = MCP::Configuration.new(validate_tool_call_results: true)
862
- server = MCP::Server.new(
863
- name: "example_server",
864
- tools: [WeatherTool],
865
- configuration: configuration
866
- )
867
- ```
868
-
869
- When enabled, successful tool responses for tools with an `output_schema` must include `structured_content` that conforms to the schema. Error responses are not validated against the output schema.
870
-
871
- ### Tool Responses with Structured Content
872
-
873
- Tools can return structured data alongside text content using the `structured_content` parameter.
874
-
875
- The structured content will be included in the JSON-RPC response as the `structuredContent` field.
876
-
877
- Per SEP-2106, `structured_content` may be any JSON value, not only an object. When a tool returns a non-object value (e.g. an array)
878
- without providing any content blocks, the server automatically mirrors it into `content` as serialized JSON text so older clients
879
- that only read `content` still receive the data.
880
-
881
- ```ruby
882
- class WeatherTool < MCP::Tool
883
- description "Get current weather and return structured data"
884
-
885
- def self.call(location:, units: "celsius", server_context:)
886
- # Call weather API and structure the response
887
- api_response = WeatherAPI.fetch(location, units)
888
- weather_data = {
889
- temperature: api_response.temp,
890
- condition: api_response.description,
891
- humidity: api_response.humidity_percent
892
- }
893
-
894
- output_schema.validate_result(weather_data)
895
-
896
- MCP::Tool::Response.new(
897
- [{
898
- type: "text",
899
- text: weather_data.to_json
900
- }],
901
- structured_content: weather_data
902
- )
903
- end
904
- end
905
- ```
906
-
907
- ### Tool Responses with Errors
908
-
909
- Tools can return error information alongside text content using the `error` parameter.
910
-
911
- The error will be included in the JSON-RPC response as the `isError` field.
912
-
913
- ```ruby
914
- class WeatherTool < MCP::Tool
915
- description "Get current weather and return structured data"
916
-
917
- def self.call(server_context:)
918
- # Do something here
919
- content = {}
920
-
921
- MCP::Tool::Response.new(
922
- [{
923
- type: "text",
924
- text: content.to_json
925
- }],
926
- structured_content: content,
927
- error: true
928
- )
929
- end
930
- end
931
- ```
932
-
933
- ### Tool Responses with Image, Audio, and Embedded Resources
934
-
935
- Tool responses are not limited to text. The `MCP::Content` module provides `Image`, `Audio`, and `EmbeddedResource` content types,
936
- which serialize to the `image`, `audio`, and `resource` content blocks defined by the MCP spec. Image and audio data is passed as
937
- a base64-encoded string together with its MIME type:
938
-
939
- ```ruby
940
- class ChartTool < MCP::Tool
941
- description "Render a chart as a PNG image"
942
-
943
- def self.call(server_context:)
944
- MCP::Tool::Response.new([
945
- MCP::Content::Text.new("Here is the rendered chart:").to_h,
946
- MCP::Content::Image.new(Base64.strict_encode64(render_chart_png), "image/png").to_h,
947
- ])
948
- end
949
- end
950
-
951
- class SpeechTool < MCP::Tool
952
- description "Synthesize speech audio"
953
-
954
- def self.call(server_context:)
955
- MCP::Tool::Response.new([
956
- MCP::Content::Audio.new(Base64.strict_encode64(synthesize_wav), "audio/wav").to_h,
957
- ])
958
- end
959
- end
960
- ```
961
-
962
- An embedded resource wraps `MCP::Resource::TextContents` or `MCP::Resource::BlobContents`, allowing a tool to return resource contents inline:
963
-
964
- ```ruby
965
- class ReportTool < MCP::Tool
966
- description "Return a report as an embedded resource"
967
-
968
- def self.call(server_context:)
969
- contents = MCP::Resource::TextContents.new(
970
- uri: "report://monthly",
971
- mime_type: "application/json",
972
- text: { total: 42 }.to_json,
973
- )
974
-
975
- MCP::Tool::Response.new([MCP::Content::EmbeddedResource.new(contents).to_h])
976
- end
977
- end
978
- ```
979
-
980
- ### Prompts
981
-
982
- MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
983
-
984
- The `MCP::Prompt` class provides three ways to create prompts:
985
-
986
- 1. As a class definition with metadata:
987
-
988
- ```ruby
989
- class MyPrompt < MCP::Prompt
990
- prompt_name "my_prompt" # Optional - defaults to underscored class name
991
- title "My Prompt"
992
- description "This prompt performs specific functionality..."
993
- arguments [
994
- MCP::Prompt::Argument.new(
995
- name: "message",
996
- title: "Message Title",
997
- description: "Input message",
998
- required: true
999
- )
1000
- ]
1001
- meta({ version: "1.0", category: "example" })
1002
-
1003
- class << self
1004
- def template(args, server_context:)
1005
- MCP::Prompt::Result.new(
1006
- description: "Response description",
1007
- messages: [
1008
- MCP::Prompt::Message.new(
1009
- role: "user",
1010
- content: MCP::Content::Text.new("User message")
1011
- ),
1012
- MCP::Prompt::Message.new(
1013
- role: "assistant",
1014
- content: MCP::Content::Text.new(args["message"])
1015
- )
1016
- ]
1017
- )
1018
- end
1019
- end
1020
- end
1021
-
1022
- prompt = MyPrompt
1023
- ```
1024
-
1025
- 2. Using the `MCP::Prompt.define` method:
1026
-
1027
- ```ruby
1028
- prompt = MCP::Prompt.define(
1029
- name: "my_prompt",
1030
- title: "My Prompt",
1031
- description: "This prompt performs specific functionality...",
1032
- arguments: [
1033
- MCP::Prompt::Argument.new(
1034
- name: "message",
1035
- title: "Message Title",
1036
- description: "Input message",
1037
- required: true
1038
- )
1039
- ],
1040
- meta: { version: "1.0", category: "example" }
1041
- ) do |args, server_context:|
1042
- MCP::Prompt::Result.new(
1043
- description: "Response description",
1044
- messages: [
1045
- MCP::Prompt::Message.new(
1046
- role: "user",
1047
- content: MCP::Content::Text.new("User message")
1048
- ),
1049
- MCP::Prompt::Message.new(
1050
- role: "assistant",
1051
- content: MCP::Content::Text.new(args["message"])
1052
- )
1053
- ]
1054
- )
1055
- end
1056
- ```
1057
-
1058
- 3. Using the `MCP::Server#define_prompt` method:
1059
-
1060
- ```ruby
1061
- server = MCP::Server.new
1062
- server.define_prompt(
1063
- name: "my_prompt",
1064
- description: "This prompt performs specific functionality...",
1065
- arguments: [
1066
- Prompt::Argument.new(
1067
- name: "message",
1068
- title: "Message Title",
1069
- description: "Input message",
1070
- required: true
1071
- )
1072
- ],
1073
- meta: { version: "1.0", category: "example" }
1074
- ) do |args, server_context:|
1075
- Prompt::Result.new(
1076
- description: "Response description",
1077
- messages: [
1078
- Prompt::Message.new(
1079
- role: "user",
1080
- content: Content::Text.new("User message")
1081
- ),
1082
- Prompt::Message.new(
1083
- role: "assistant",
1084
- content: Content::Text.new(args["message"])
1085
- )
1086
- ]
1087
- )
1088
- end
1089
- ```
1090
-
1091
- The server_context parameter is the server_context passed into the server and can be used to pass per request information,
1092
- e.g. around authentication state or user preferences.
1093
-
1094
- ### Key Components
1095
-
1096
- - `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
1097
- - `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
1098
- - `MCP::Prompt::Result` - The output of a prompt template containing description and messages
1099
- - `MCP::Content::Text` - Text content for messages
1100
-
1101
- ### Usage
1102
-
1103
- Register prompts with the MCP server:
1104
-
1105
- ```ruby
1106
- server = MCP::Server.new(
1107
- name: "my_server",
1108
- prompts: [MyPrompt],
1109
- server_context: { user_id: current_user.id },
1110
- )
1111
- ```
1112
-
1113
- The server will handle prompt listing and execution through the MCP protocol methods:
1114
-
1115
- - `prompts/list` - Lists all registered prompts and their schemas
1116
- - `prompts/get` - Retrieves and executes a specific prompt with arguments
1117
-
1118
- ### Prompts with Image and Embedded Resource Content
1119
-
1120
- Prompt messages are not limited to text. The same `MCP::Content` types used in tool responses can be used as message content,
1121
- letting a prompt template include images or inline resource contents. Unlike tool responses, the content object is passed directly rather than as a hash;
1122
- `MCP::Prompt::Message` serializes it when the prompt result is returned:
1123
-
1124
- ```ruby
1125
- class CodeReviewPrompt < MCP::Prompt
1126
- prompt_name "code_review"
1127
- description "Review a source file with an accompanying diagram"
1128
- arguments [
1129
- MCP::Prompt::Argument.new(name: "file_uri", description: "URI of the file to review", required: true),
1130
- ]
1131
-
1132
- class << self
1133
- def template(args, server_context:)
1134
- MCP::Prompt::Result.new(
1135
- messages: [
1136
- MCP::Prompt::Message.new(
1137
- role: "user",
1138
- content: MCP::Content::EmbeddedResource.new(
1139
- MCP::Resource::TextContents.new(
1140
- uri: args["file_uri"],
1141
- mime_type: "text/x-ruby",
1142
- text: read_source(args["file_uri"]),
1143
- ),
1144
- ),
1145
- ),
1146
- MCP::Prompt::Message.new(
1147
- role: "user",
1148
- content: MCP::Content::Image.new(architecture_diagram_base64, "image/png"),
1149
- ),
1150
- MCP::Prompt::Message.new(
1151
- role: "user",
1152
- content: MCP::Content::Text.new("Please review the code above, using the diagram for context."),
1153
- ),
1154
- ],
1155
- )
1156
- end
1157
- end
1158
- end
1159
- ```
1160
-
1161
- ### Resources
1162
-
1163
- MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
1164
-
1165
- ### Reading Resources
1166
-
1167
- Like tools and prompts, resources can be defined in three ways.
1168
-
1169
- 1. As a class that inherits from `MCP::Resource`, implementing `contents` to serve the resource body:
1170
-
1171
- ```ruby
1172
- class MyResource < MCP::Resource
1173
- uri "https://example.com/my_resource"
1174
- resource_name "my-resource"
1175
- title "My Resource"
1176
- description "Lorem ipsum dolor sit amet"
1177
- mime_type "text/html"
1178
-
1179
- class << self
1180
- def contents
1181
- [MCP::Resource::TextContents.new(
1182
- uri: uri,
1183
- mime_type: mime_type,
1184
- text: "Hello from example resource!"
1185
- )]
1186
- end
1187
- end
1188
- end
1189
-
1190
- server = MCP::Server.new(
1191
- name: "my_server",
1192
- resources: [MyResource],
1193
- )
1194
- ```
1195
-
1196
- `resources/read` requests are routed automatically: when the requested URI matches a registered
1197
- class-based resource, its `contents` method is called. `contents` may return an array of
1198
- `MCP::Resource::TextContents` / `MCP::Resource::BlobContents` objects (or plain hashes), or a single one.
1199
- Like tools, `contents` can opt in to a `server_context:` keyword argument to receive per-request context.
1200
-
1201
- When class-based resources or resource templates are registered and a `resources/read` request
1202
- does not match any of them, the server responds with the standard JSON-RPC Invalid Params error
1203
- (`-32602`) carrying the requested URI in the error `data` member, per SEP-2164.
1204
-
1205
- 2. With the `MCP::Resource.define` method, whose block implements `contents`:
1206
-
1207
- ```ruby
1208
- resource = MCP::Resource.define(
1209
- uri: "https://example.com/my_resource",
1210
- name: "my-resource",
1211
- mime_type: "text/html",
1212
- ) do
1213
- [MCP::Resource::TextContents.new(uri: uri, mime_type: mime_type, text: "Hello!")]
1214
- end
1215
- ```
1216
-
1217
- 3. Using the `MCP::Server#define_resource` method:
1218
-
1219
- ```ruby
1220
- server = MCP::Server.new(name: "my_server")
1221
- server.define_resource(
1222
- uri: "https://example.com/my_resource",
1223
- name: "my-resource",
1224
- mime_type: "text/html",
1225
- ) do
1226
- [MCP::Resource::TextContents.new(uri: "https://example.com/my_resource", mime_type: "text/html", text: "Hello!")]
1227
- end
1228
- ```
1229
-
1230
- Alternatively, resources can be registered as plain data objects with `MCP::Resource.new`,
1231
- in which case the server only lists them:
1232
-
1233
- ```ruby
1234
- resource = MCP::Resource.new(
1235
- uri: "https://example.com/my_resource",
1236
- name: "my-resource",
1237
- title: "My Resource",
1238
- description: "Lorem ipsum dolor sit amet",
1239
- mime_type: "text/html",
1240
- )
1241
-
1242
- server = MCP::Server.new(
1243
- name: "my_server",
1244
- resources: [resource],
1245
- )
1246
- ```
1247
-
1248
- With plain data resources, the server must register a handler for the `resources/read` method to
1249
- retrieve a resource dynamically.
1250
-
1251
- ```ruby
1252
- server.resources_read_handler do |params|
1253
- [{
1254
- uri: params[:uri],
1255
- mimeType: "text/plain",
1256
- text: "Hello from example resource! URI: #{params[:uri]}"
1257
- }]
1258
- end
1259
- ```
1260
-
1261
- otherwise `resources/read` requests will be a no-op. Note that a `resources_read_handler` fully replaces
1262
- the default `resources/read` handling, including the automatic routing to class-based resources described above.
1263
-
1264
- For unknown URIs, raise `MCP::Server::ResourceNotFoundError` from the handler.
1265
- Per SEP-2164, the server then responds with the standard JSON-RPC Invalid Params error (`-32602`)
1266
- carrying the requested URI in the error `data` member:
1267
-
1268
- ```ruby
1269
- server.resources_read_handler do |params|
1270
- resource = lookup(params[:uri])
1271
- raise MCP::Server::ResourceNotFoundError.new(params[:uri], params) unless resource
1272
-
1273
- [{ uri: params[:uri], mimeType: resource.mime_type, text: resource.body }]
1274
- end
1275
- ```
1276
-
1277
- ### Reading Binary Resources
1278
-
1279
- For binary resources, respond with a base64-encoded `blob` field instead of `text`.
1280
- The `MCP::Resource::TextContents` and `MCP::Resource::BlobContents` classes build the two contents shapes defined by the spec:
1281
-
1282
- ```ruby
1283
- server.resources_read_handler do |params|
1284
- case params[:uri]
1285
- when "file:///logo.png"
1286
- [
1287
- MCP::Resource::BlobContents.new(
1288
- uri: params[:uri],
1289
- mime_type: "image/png",
1290
- data: Base64.strict_encode64(File.binread("logo.png")),
1291
- ).to_h,
1292
- ]
1293
- else
1294
- [
1295
- MCP::Resource::TextContents.new(
1296
- uri: params[:uri],
1297
- mime_type: "text/plain",
1298
- text: "Hello from example resource!",
1299
- ).to_h,
1300
- ]
1301
- end
1302
- end
1303
- ```
1304
-
1305
- ### Resource Templates
1306
-
1307
- Resource templates follow the same pattern. Class-based templates declare a `uri_template` and
1308
- receive the variables extracted from the requested URI as keyword arguments to `contents`:
1309
-
1310
- ```ruby
1311
- class UserProfileTemplate < MCP::ResourceTemplate
1312
- uri_template "users://{user_id}/profile"
1313
- resource_template_name "user-profile"
1314
- title "User Profile"
1315
- description "Profile data for a user"
1316
- mime_type "application/json"
1317
-
1318
- class << self
1319
- def contents(user_id:)
1320
- [MCP::Resource::TextContents.new(
1321
- uri: "users://#{user_id}/profile",
1322
- mime_type: mime_type,
1323
- text: { id: user_id }.to_json
1324
- )]
1325
- end
1326
- end
1327
- end
1328
-
1329
- server = MCP::Server.new(
1330
- name: "my_server",
1331
- resource_templates: [UserProfileTemplate],
1332
- )
1333
- ```
1334
-
1335
- A `resources/read` request for `users://42/profile` calls `UserProfileTemplate.contents(user_id: "42")`.
1336
- An exact match against a registered resource takes precedence over template matching.
1337
- `contents` can also opt in to a `server_context:` keyword argument.
1338
-
1339
- URI template matching supports simple [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) level 1 `{variable}` expressions only:
1340
-
1341
- - Operator expressions such as `{+path}`, `{#fragment}`, or `{?query}` are treated as literal text and never match an expanded URI.
1342
- - A variable matches one or more characters excluding `/`.
1343
- - Extracted values are not percent-decoded.
1344
-
1345
- The `MCP::ResourceTemplate.define` and `MCP::Server#define_resource_template` methods are also available,
1346
- mirroring the resource variants:
1347
-
1348
- ```ruby
1349
- server.define_resource_template(
1350
- uri_template: "users://{user_id}/profile",
1351
- name: "user-profile",
1352
- mime_type: "application/json",
1353
- ) do |user_id:|
1354
- [MCP::Resource::TextContents.new(
1355
- uri: "users://#{user_id}/profile",
1356
- mime_type: "application/json",
1357
- text: { id: user_id }.to_json
1358
- )]
1359
- end
1360
- ```
1361
-
1362
- Resource templates can also be registered as plain data objects with `MCP::ResourceTemplate.new`,
1363
- in which case reads must be served by a `resources_read_handler`:
1364
-
1365
- ```ruby
1366
- resource_template = MCP::ResourceTemplate.new(
1367
- uri_template: "https://example.com/my_resource_template",
1368
- name: "my-resource-template",
1369
- title: "My Resource Template",
1370
- description: "Lorem ipsum dolor sit amet",
1371
- mime_type: "text/html",
1372
- )
1373
-
1374
- server = MCP::Server.new(
1375
- name: "my_server",
1376
- resource_templates: [resource_template],
1377
- )
1378
- ```
1379
-
1380
- Registered templates are listed through the `resources/templates/list` protocol method.
1381
- To serve reads for URIs that match a template, extract the variable parts of the URI in your `resources_read_handler`:
1382
-
1383
- ```ruby
1384
- resource_template = MCP::ResourceTemplate.new(
1385
- uri_template: "file:///items/{item_id}",
1386
- name: "item",
1387
- mime_type: "application/json",
1388
- )
1389
-
1390
- server = MCP::Server.new(name: "my_server", resource_templates: [resource_template])
1391
-
1392
- server.resources_read_handler do |params|
1393
- if (match = params[:uri].match(%r{\Afile:///items/(?<item_id>[^/]+)\z}))
1394
- [{
1395
- uri: params[:uri],
1396
- mimeType: "application/json",
1397
- text: { id: match[:item_id] }.to_json,
1398
- }]
1399
- else
1400
- raise MCP::Server::ResourceNotFoundError.new(params[:uri], params)
1401
- end
1402
- end
1403
- ```
1404
-
1405
- ### Roots
1406
-
1407
- The Model Context Protocol allows servers to request filesystem roots from clients through the `roots/list` method.
1408
- Roots define the boundaries of where a server can operate, providing a list of directories and files the client has made available.
1409
-
1410
- **Key Concepts:**
1411
-
1412
- - **Server-to-Client Request**: Like sampling, roots listing is initiated by the server
1413
- - **Client Capability**: Clients must declare `roots` capability during initialization
1414
- - **Change Notifications**: Clients that support `roots.listChanged` send `notifications/roots/list_changed` when roots change
1415
-
1416
- > [!NOTE]
1417
- > Per SEP-2260, server-to-client requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) must be associated with
1418
- > an originating client request (`ping` is exempt). Use the `server_context` passed to your handler, which stamps the association
1419
- > automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding
1420
- > `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning.
1421
-
1422
- **Timeouts:** every server-to-client request is bounded, so a client that never answers cannot park the handler's thread indefinitely.
1423
- `MCP::Server::Transports::StreamableHTTPTransport` waits `server_to_client_request_timeout:` seconds (600 by default), then tells
1424
- the client the request was abandoned and raises `MCP::Server::RequestTimeoutError`. Individual calls override the deadline with `timeout:`,
1425
- which is the knob to reach for when a prompt legitimately waits on a person:
1426
-
1427
- ```ruby
1428
- server_context.create_form_elicitation(
1429
- message: "Approve this deployment?",
1430
- requested_schema: { type: "object", properties: { approved: { type: "boolean" } } },
1431
- timeout: 3600, # This one waits up to an hour.
1432
- )
1433
- ```
1434
-
1435
- `StdioTransport` is not bounded and ignores `timeout:`: it owns the client process, so a client that stops answering
1436
- surfaces as end-of-file rather than as a wait that never ends.
1437
-
1438
- **Using Roots in Tools:**
1439
-
1440
- Tools that accept a `server_context:` parameter can call `list_roots` on it.
1441
- The request is automatically routed to the correct client session:
1442
-
1443
- ```ruby
1444
- class FileSearchTool < MCP::Tool
1445
- description "Search files within the client's project roots"
1446
- input_schema(
1447
- properties: {
1448
- query: { type: "string" }
1449
- },
1450
- required: ["query"]
1451
- )
1452
-
1453
- def self.call(query:, server_context:)
1454
- roots = server_context.list_roots
1455
- root_uris = roots[:roots].map { |root| root[:uri] }
1456
-
1457
- MCP::Tool::Response.new([{
1458
- type: "text",
1459
- text: "Searching in roots: #{root_uris.join(", ")}"
1460
- }])
1461
- end
1462
- end
1463
- ```
1464
-
1465
- Result contains an array of root objects:
1466
-
1467
- ```ruby
1468
- {
1469
- roots: [
1470
- { uri: "file:///home/user/projects/myproject", name: "My Project" },
1471
- { uri: "file:///home/user/repos/backend", name: "Backend Repository" }
1472
- ]
1473
- }
1474
- ```
1475
-
1476
- **Handling Root Changes:**
1477
-
1478
- Register a callback to be notified when the client's roots change:
1479
-
1480
- ```ruby
1481
- server.roots_list_changed_handler do
1482
- puts "Client's roots have changed, tools will see updated roots on next call."
1483
- end
1484
- ```
1485
-
1486
- **Error Handling:**
1487
-
1488
- - Raises `RuntimeError` if client does not support `roots` capability
1489
- - Raises `StandardError` if client returns an error response
1490
-
1491
- ### Resource Subscriptions
1492
-
1493
- Resource subscriptions allow clients to monitor specific resources for changes.
1494
- When a subscribed resource is updated, the server sends a notification to the client.
1495
-
1496
- The SDK does not track subscription state internally.
1497
- Server developers register handlers and manage their own subscription state.
1498
- Three methods are provided:
1499
-
1500
- - `Server#resources_subscribe_handler` - registers a handler for `resources/subscribe` requests
1501
- - `Server#resources_unsubscribe_handler` - registers a handler for `resources/unsubscribe` requests
1502
- - `ServerContext#notify_resources_updated` - sends a `notifications/resources/updated` notification to the subscribing client
1503
-
1504
- ```ruby
1505
- subscribed_uris = Set.new
1506
-
1507
- server = MCP::Server.new(
1508
- name: "my_server",
1509
- resources: [my_resource],
1510
- capabilities: { resources: { subscribe: true } },
1511
- )
1512
-
1513
- server.resources_subscribe_handler do |params|
1514
- subscribed_uris.add(params[:uri].to_s)
1515
- end
1516
-
1517
- server.resources_unsubscribe_handler do |params|
1518
- subscribed_uris.delete(params[:uri].to_s)
1519
- end
1520
-
1521
- server.define_tool(name: "update_resource") do |server_context:, **args|
1522
- if subscribed_uris.include?("test://my-resource")
1523
- server_context.notify_resources_updated(uri: "test://my-resource")
1524
- end
1525
- MCP::Tool::Response.new([MCP::Content::Text.new("Resource updated").to_h])
1526
- end
1527
- ```
1528
-
1529
- ### Sampling
1530
-
1531
- The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.
1532
- This enables servers to leverage the client's LLM capabilities without needing direct access to AI models.
1533
-
1534
- **Key Concepts:**
1535
-
1536
- - **Server-to-Client Request**: Unlike typical MCP methods (client to server), sampling is initiated by the server
1537
- - **Client Capability**: Clients must declare `sampling` capability during initialization
1538
- - **Tool Support**: When using tools in sampling requests, clients must declare `sampling.tools` capability
1539
- - **Human-in-the-Loop**: Clients can implement user approval before forwarding requests to LLMs
1540
-
1541
- **Using Sampling in Tools:**
1542
-
1543
- Tools that accept a `server_context:` parameter can call `create_sampling_message` on it.
1544
- The request is automatically routed to the correct client session:
1545
-
1546
- ```ruby
1547
- class SummarizeTool < MCP::Tool
1548
- description "Summarize text using LLM"
1549
- input_schema(
1550
- properties: {
1551
- text: { type: "string" }
1552
- },
1553
- required: ["text"]
1554
- )
1555
-
1556
- def self.call(text:, server_context:)
1557
- result = server_context.create_sampling_message(
1558
- messages: [
1559
- { role: "user", content: { type: "text", text: "Please summarize: #{text}" } }
1560
- ],
1561
- max_tokens: 500
1562
- )
1563
-
1564
- MCP::Tool::Response.new([{
1565
- type: "text",
1566
- text: result[:content][:text]
1567
- }])
1568
- end
1569
- end
1570
-
1571
- server = MCP::Server.new(name: "my_server", tools: [SummarizeTool])
1572
- ```
1573
-
1574
- **Parameters:**
1575
-
1576
- Required:
1577
-
1578
- - `messages:` (Array) - Array of message objects with `role` and `content`
1579
- - `max_tokens:` (Integer) - Maximum tokens in the response
1580
-
1581
- Optional:
1582
-
1583
- - `system_prompt:` (String) - System prompt for the LLM
1584
- - `model_preferences:` (Hash) - Model selection preferences (e.g., `{ intelligencePriority: 0.8 }`)
1585
- - `include_context:` (String) - Context inclusion: `"none"`, `"thisServer"`, or `"allServers"` (soft-deprecated)
1586
- - `temperature:` (Float) - Sampling temperature
1587
- - `stop_sequences:` (Array) - Sequences that stop generation
1588
- - `metadata:` (Hash) - Additional metadata
1589
- - `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
1590
- - `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)
1591
-
1592
- **Error Handling:**
1593
-
1594
- - Raises `RuntimeError` if client does not support `sampling` capability
1595
- - Raises `RuntimeError` if `tools` are used but client lacks `sampling.tools` capability
1596
- - Raises `StandardError` if client returns an error response
1597
-
1598
- ### Notifications
1599
-
1600
- The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
1601
-
1602
- #### Notification Methods
1603
-
1604
- The server provides the following notification methods:
1605
-
1606
- - `notify_tools_list_changed` - Send a notification when the tools list changes
1607
- - `notify_prompts_list_changed` - Send a notification when the prompts list changes
1608
- - `notify_resources_list_changed` - Send a notification when the resources list changes
1609
- - `notify_log_message` - Send a structured logging notification message
1610
-
1611
- #### Session Scoping
1612
-
1613
- When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
1614
-
1615
- - **`report_progress`** and **`notify_log_message`** called via `server_context` inside a tool handler are automatically sent only to the requesting client.
1616
- No extra configuration is needed.
1617
- - **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
1618
- as they represent server-wide state changes. These should be called on the `server` instance directly.
1619
-
1620
- #### Notification Format
1621
-
1622
- Notifications follow the JSON-RPC 2.0 specification and use these method names:
1623
-
1624
- - `notifications/tools/list_changed`
1625
- - `notifications/prompts/list_changed`
1626
- - `notifications/resources/list_changed`
1627
- - `notifications/cancelled`
1628
- - `notifications/progress`
1629
- - `notifications/message`
1630
-
1631
- ### Cancellation
1632
-
1633
- The MCP Ruby SDK supports server-side handling of the
1634
- [MCP `notifications/cancelled` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation).
1635
- When a client sends `notifications/cancelled` for an in-flight request, the server stops
1636
- processing cooperatively and suppresses the JSON-RPC response for that request.
1637
-
1638
- Cancellation is cooperative: the SDK does not forcibly terminate tool code. Instead,
1639
- a `MCP::Cancellation` token is threaded through `server_context`, and long-running tools
1640
- poll it to exit early. When a tool returns after cancellation has been observed,
1641
- the server suppresses the JSON-RPC response, matching the spec. The `initialize` request
1642
- is never cancellable per the spec.
1643
-
1644
- Client-initiated cancellation is also supported: see [Client-Side: Cancelling an In-Flight Request](#client-side-cancelling-an-in-flight-request) below.
1645
-
1646
- #### Server-Side: Handlers that Check for Cancellation
1647
-
1648
- Any handler that opts in to `server_context:` - tools (`Tool.call`), prompt templates,
1649
- `resources_read_handler`, `completion_handler`, `resources_subscribe_handler`,
1650
- `resources_unsubscribe_handler`, and `define_custom_method` blocks - receives
1651
- an `MCP::ServerContext` wired to the in-flight request's cancellation token.
1652
- Handlers check `cancelled?` in their work loop, or call `raise_if_cancelled!` to raise
1653
- `MCP::CancelledError` at a safe point:
1654
-
1655
- ```ruby
1656
- class LongRunningTool < MCP::Tool
1657
- description "A tool that supports cancellation"
1658
- input_schema(properties: { count: { type: "integer" } }, required: ["count"])
1659
-
1660
- def self.call(count:, server_context:)
1661
- count.times do |i|
1662
- # Exit early if the client has sent `notifications/cancelled`.
1663
- break if server_context.cancelled?
1664
-
1665
- do_work(i)
1666
- end
1667
-
1668
- MCP::Tool::Response.new([{ type: "text", text: "Done" }])
1669
- end
1670
- end
1671
- ```
1672
-
1673
- Alternatively, raise at the next safe point with `raise_if_cancelled!`:
1674
-
1675
- ```ruby
1676
- def self.call(count:, server_context:)
1677
- count.times do |i|
1678
- server_context.raise_if_cancelled!
1679
-
1680
- do_work(i)
1681
- end
1682
-
1683
- MCP::Tool::Response.new([{ type: "text", text: "Done" }])
1684
- end
1685
- ```
1686
-
1687
- When a handler observes cancellation (either by returning early with `cancelled?` or
1688
- by raising `MCP::CancelledError` via `raise_if_cancelled!`), the server drops the response and
1689
- no JSON-RPC result is sent to the client.
1690
-
1691
- The same pattern works for other handler types:
1692
-
1693
- ```ruby
1694
- # resources/read
1695
- server.resources_read_handler do |params, server_context:|
1696
- server_context.raise_if_cancelled!
1697
- # read the resource
1698
- end
1699
-
1700
- # completion/complete
1701
- server.completion_handler do |params, server_context:|
1702
- server_context.raise_if_cancelled!
1703
- # compute completions
1704
- end
1705
-
1706
- # custom method
1707
- server.define_custom_method(method_name: "custom/slow") do |params, server_context:|
1708
- server_context.raise_if_cancelled!
1709
- # do work
1710
- end
1711
-
1712
- # prompts (via Prompt subclass)
1713
- class SlowPrompt < MCP::Prompt
1714
- prompt_name "slow_prompt"
1715
-
1716
- def self.template(args, server_context:)
1717
- server_context.raise_if_cancelled!
1718
- MCP::Prompt::Result.new(messages: [])
1719
- end
1720
- end
1721
- ```
1722
-
1723
- Handlers that do not declare a `server_context:` keyword continue to work unchanged -
1724
- the opt-in detection only wraps the context when the block signature asks for it.
1725
-
1726
- #### Nested Server-to-Client Requests Are Cancelled Automatically
1727
-
1728
- When a tool handler is waiting on a nested server-to-client request
1729
- (`server_context.create_sampling_message`, `create_form_elicitation`, or
1730
- `create_url_elicitation`), cancelling the parent tool call automatically raises
1731
- `MCP::CancelledError` from the nested call, so the tool does not need to wrap it
1732
- in its own `cancelled?` checks:
1733
-
1734
- ```ruby
1735
- def self.call(server_context:)
1736
- result = server_context.create_sampling_message(messages: messages, max_tokens: 100)
1737
- # If the parent tools/call is cancelled while waiting above, MCP::CancelledError
1738
- # is raised here and the tool can let it propagate or clean up as needed.
1739
- MCP::Tool::Response.new([{ type: "text", text: result[:content][:text] }])
1740
- rescue MCP::CancelledError
1741
- # Optional: run cleanup. Re-raising (or letting it propagate) is fine; the server
1742
- # will still suppress the JSON-RPC response per the MCP spec.
1743
- raise
1744
- end
1745
- ```
1746
-
1747
- Nested cancellation propagation is supported on `StreamableHTTPTransport` only.
1748
- `StdioTransport` is single-threaded and blocks on `$stdin.gets`, so a nested
1749
- `server_context.create_sampling_message` inside a tool runs to completion even if
1750
- the parent `tools/call` is cancelled. The parent tool itself still observes cancellation
1751
- via `server_context.cancelled?` between nested calls.
1752
-
1753
- #### Client-Side: Cancelling an In-Flight Request
1754
-
1755
- `MCP::Client` lets the caller cancel a request it has already issued. The recommended pattern is to pass
1756
- an `MCP::Cancellation` token into the request method, run the request on a worker thread, and call
1757
- `cancellation.cancel(reason:)` from another thread. The cancelling thread sends `notifications/cancelled` to
1758
- the server, and the calling thread is woken up with `MCP::CancelledError`:
1759
-
1760
- ```ruby
1761
- client = MCP::Client.new(transport: transport)
1762
- cancellation = MCP::Cancellation.new
1763
-
1764
- Thread.new do
1765
- client.call_tool(name: "slow_tool", arguments: {}, cancellation: cancellation)
1766
- rescue MCP::CancelledError
1767
- # cleanup
1768
- end
1769
-
1770
- # Later, from another thread:
1771
- cancellation.cancel(reason: "user pressed cancel")
1772
- ```
1773
-
1774
- All request methods (`tools`, `list_tools`, `resources`, `list_resources`, `resource_templates`, `list_resource_templates`,
1775
- `prompts`, `list_prompts`, `call_tool`, `read_resource`, `get_prompt`, `complete`, `ping`) accept the `cancellation:` keyword.
1776
- Request ids are managed internally, so the token is the only thing a caller needs to cancel a request.
1777
-
1778
- > [!NOTE]
1779
- > When a cancel wins the race, the SDK's worker thread that is blocked on the underlying I/O is *not* force-killed;
1780
- > it stays blocked until the transport actually returns (or the user closes the transport). This matches the server-side
1781
- > `StreamableHTTPTransport#send_request` trade-off. For `StreamableHTTPTransport#send_request` trade-off. For `Client::HTTP`
1782
- > the leak resolves as soon as the server sends any response; for `Client::Stdio` you may need to call `client.transport.close`
1783
- > to free the thread if the server stops responding entirely. The cancel-dispatch thread waits for the worker's send-boundary signal
1784
- > (`&on_sent` from `send_request`) before issuing `notifications/cancelled`, so the cancel is held until the worker has at
1785
- > least committed to writing the request; while the worker is wedged the cancel notification is deferred along with it.
1786
-
1787
- ##### Wire-order guarantees
1788
-
1789
- `Client::Stdio` serializes the request write and any subsequent `notifications/cancelled` write through a single `@write_mutex`,
1790
- so the server is guaranteed to read the request line before the cancel line.
1791
-
1792
- `Client::HTTP` cannot offer the same wire-arrival guarantee. Faraday's synchronous `post` does not expose a post-write / pre-response hook,
1793
- so the SDK yields just before the request POST is dispatched. After the yield, the cancel-dispatch thread issues a separate `notifications/cancelled` POST
1794
- on its own connection, and the two POSTs may overlap on the network. The spec is satisfied either way: the sender has already issued the request and
1795
- still believes it to be in-progress when issuing the cancel ([MCP cancellation spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation)),
1796
- and on the receiver side, "receivers MAY ignore a cancellation notification whose `requestId` is unknown" covers the case where the cancel POST
1797
- happens to arrive first. The calling thread raises `MCP::CancelledError` regardless of network ordering.
1798
-
1799
- ##### Custom transports
1800
-
1801
- Custom transports that want to support `cancellation:` must implement `send_notification(notification:)` so `notifications/cancelled` can be delivered.
1802
- They should also accept the optional block passed to `send_request(request:, &on_sent)` and call it once the request bytes have been handed off to the wire
1803
- (under a write-side mutex for stdio-style transports, immediately before the synchronous round-trip for HTTP-style transports).
1804
- The cancel-dispatch thread waits on this signal before sending `notifications/cancelled`. Transports that do not invoke the block fall back to waiting for
1805
- the worker thread to terminate, which preserves wire-order at the cost of delaying the cancel notification until the request has fully completed.
1806
-
1807
- ### Ping
1808
-
1809
- The MCP Ruby SDK supports the
1810
- [MCP `ping` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping),
1811
- which allows either side of the connection to verify that the peer is still responsive.
1812
- A `ping` request has no parameters, and the receiver MUST respond promptly with an empty result.
1813
-
1814
- #### Server-Side
1815
-
1816
- Servers respond to incoming `ping` requests automatically - no setup is required.
1817
- Any `MCP::Server` instance replies with an empty result.
1818
-
1819
- Servers can also send `ping` requests to the client via `ServerSession#ping`.
1820
- Inside a tool handler that receives `server_context:`, call `ping` on it:
1821
-
1822
- ```ruby
1823
- class HealthCheckTool < MCP::Tool
1824
- description "Verifies the client is still responsive"
1825
-
1826
- def self.call(server_context:)
1827
- server_context.ping # => {} on success
1828
-
1829
- MCP::Tool::Response.new([{ type: "text", text: "client is alive" }])
1830
- end
1831
- end
1832
- ```
1833
-
1834
- `#ping` raises `MCP::Server::ValidationError` when the client returns a `result`
1835
- that is not a Hash. Transport-level errors (e.g., the client returning a JSON-RPC error)
1836
- propagate as exceptions raised by the transport layer.
1837
-
1838
- #### Client-Side
1839
-
1840
- `MCP::Client` exposes `ping` to send a ping to the server:
1841
-
1842
- ```ruby
1843
- client = MCP::Client.new(transport: transport)
1844
- client.ping # => {} on success
1845
- ```
1846
-
1847
- `#ping` raises `MCP::Client::ServerError` when the server returns a JSON-RPC error.
1848
- It raises `MCP::Client::ValidationError` when the response `result` is missing or
1849
- is not a Hash (matching the spec requirement that `result` be an object).
1850
- Transport-level errors (for example, `MCP::Client::Stdio`'s `read_timeout:` firing)
1851
- propagate as exceptions raised by the transport layer.
1852
-
1853
- ### Progress
1854
-
1855
- The MCP Ruby SDK supports progress tracking for long-running tool operations,
1856
- following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
1857
-
1858
- #### How Progress Works
1859
-
1860
- 1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
1861
- 2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
1862
- 3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
1863
-
1864
- #### Server-Side: Tool with Progress
1865
-
1866
- Tools that accept a `server_context:` parameter can call `report_progress` on it.
1867
- The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
1868
-
1869
- ```ruby
1870
- class LongRunningTool < MCP::Tool
1871
- description "A tool that reports progress during execution"
1872
- input_schema(
1873
- properties: {
1874
- count: { type: "integer" },
1875
- },
1876
- required: ["count"]
1877
- )
1878
-
1879
- def self.call(count:, server_context:)
1880
- count.times do |i|
1881
- # Do work here.
1882
- server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
1883
- end
1884
-
1885
- MCP::Tool::Response.new([{ type: "text", text: "Done" }])
1886
- end
1887
- end
1888
- ```
1889
-
1890
- The `server_context.report_progress` method accepts:
1891
-
1892
- - `progress` (required) — current progress value (numeric)
1893
- - `total:` (optional) — total expected value, so clients can display a percentage
1894
- - `message:` (optional) — human-readable status message
1895
-
1896
- **Key Features:**
1897
-
1898
- - Tools report progress via `server_context.report_progress`
1899
- - `report_progress` is a no-op when no `progressToken` was provided by the client
1900
- - Supports both numeric and string progress tokens
1901
-
1902
- ### Completions
1903
-
1904
- MCP spec includes [Completions](https://modelcontextprotocol.io/specification/latest/server/utilities/completion),
1905
- which enable servers to provide autocompletion suggestions for prompt arguments and resource URIs.
1906
-
1907
- To enable completions, declare the `completions` capability and register a handler:
1908
-
1909
- ```ruby
1910
- server = MCP::Server.new(
1911
- name: "my_server",
1912
- prompts: [CodeReviewPrompt],
1913
- resource_templates: [FileTemplate],
1914
- capabilities: { completions: {} },
1915
- )
1916
-
1917
- server.completion_handler do |params|
1918
- ref = params[:ref]
1919
- argument = params[:argument]
1920
- value = argument[:value]
1921
-
1922
- case ref[:type]
1923
- when "ref/prompt"
1924
- values = case argument[:name]
1925
- when "language"
1926
- ["python", "pytorch", "pyside"].select { |v| v.start_with?(value) }
1927
- else
1928
- []
1929
- end
1930
- { completion: { values: values, hasMore: false } }
1931
- when "ref/resource"
1932
- { completion: { values: [], hasMore: false } }
1933
- end
1934
- end
1935
- ```
1936
-
1937
- The handler receives a `params` hash with:
1938
-
1939
- - `ref` - The reference (`{ type: "ref/prompt", name: "..." }` or `{ type: "ref/resource", uri: "..." }`)
1940
- - `argument` - The argument being completed (`{ name: "...", value: "..." }`)
1941
- - `context` (optional) - Previously resolved arguments (`{ arguments: { ... } }`)
1942
-
1943
- The handler must return a hash with a `completion` key containing `values` (array of strings), and optionally `total` and `hasMore`.
1944
- The SDK automatically enforces the 100-item limit per the MCP specification.
1945
-
1946
- The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
1947
- Requests for unknown references return an error.
1948
-
1949
- ### Elicitation
1950
-
1951
- The MCP Ruby SDK supports [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation),
1952
- which allows servers to request additional information from users through the client during tool execution.
1953
-
1954
- Elicitation is a **server-to-client request**. The server sends a request and blocks until the user responds via the client.
1955
-
1956
- #### Capabilities
1957
-
1958
- Clients must declare the `elicitation` capability during initialization. The server checks this before sending any elicitation request
1959
- and raises a `RuntimeError` if the client does not support it.
1960
-
1961
- For URL mode support, the client must also declare `elicitation.url` capability.
1962
-
1963
- #### Using Elicitation in Tools
1964
-
1965
- Tools that accept a `server_context:` parameter can call `create_form_elicitation` on it:
1966
-
1967
- ```ruby
1968
- server.define_tool(name: "collect_info", description: "Collect user info") do |server_context:|
1969
- result = server_context.create_form_elicitation(
1970
- message: "Please provide your name",
1971
- requested_schema: {
1972
- type: "object",
1973
- properties: { name: { type: "string" } },
1974
- required: ["name"],
1975
- },
1976
- )
1977
-
1978
- MCP::Tool::Response.new([{ type: "text", text: "Hello, #{result[:content][:name]}" }])
1979
- end
1980
- ```
1981
-
1982
- #### Form Mode
1983
-
1984
- Form mode collects structured data from the user directly through the MCP client:
1985
-
1986
- ```ruby
1987
- server.define_tool(name: "collect_contact", description: "Collect contact info") do |server_context:|
1988
- result = server_context.create_form_elicitation(
1989
- message: "Please provide your contact information",
1990
- requested_schema: {
1991
- type: "object",
1992
- properties: {
1993
- name: { type: "string", description: "Your full name" },
1994
- email: { type: "string", format: "email", description: "Your email address" },
1995
- },
1996
- required: ["name", "email"],
1997
- },
1998
- )
1999
-
2000
- text = case result[:action]
2001
- when "accept"
2002
- "Hello, #{result[:content][:name]} (#{result[:content][:email]})"
2003
- when "decline"
2004
- "User declined"
2005
- when "cancel"
2006
- "User cancelled"
2007
- end
2008
-
2009
- MCP::Tool::Response.new([{ type: "text", text: text }])
2010
- end
2011
- ```
2012
-
2013
- The `requested_schema` must be a flat object schema: a top-level `type: "object"` whose `properties` are limited to
2014
- primitive types (`string`, `number`, `integer`, `boolean`). Nested objects and arrays are not allowed, which keeps
2015
- the schema simple enough for clients to render as a form. Per the MCP specification, the client validates
2016
- the user's input against this schema before returning it, so the `content` of an `accept` response matches the requested shape.
2017
-
2018
- #### Default Values and Enums
2019
-
2020
- Properties may declare a `default` value (SEP-1034), which clients use to pre-fill the form.
2021
- String properties may declare `enum` values, optionally with human-readable `enumNames` (SEP-1330), which clients render as a choice list:
2022
-
2023
- ```ruby
2024
- server.define_tool(name: "configure_deploy", description: "Configure a deployment") do |server_context:|
2025
- result = server_context.create_form_elicitation(
2026
- message: "Configure the deployment",
2027
- requested_schema: {
2028
- type: "object",
2029
- properties: {
2030
- replicas: { type: "integer", default: 3 },
2031
- verbose: { type: "boolean", default: false },
2032
- environment: {
2033
- type: "string",
2034
- enum: ["dev", "staging", "prod"],
2035
- enumNames: ["Development", "Staging", "Production"],
2036
- default: "dev",
2037
- },
2038
- },
2039
- required: ["environment"],
2040
- },
2041
- )
2042
-
2043
- MCP::Tool::Response.new([{ type: "text", text: "Deploying to #{result[:content][:environment]}" }])
2044
- end
2045
- ```
2046
-
2047
- #### Enum Schemas
2048
-
2049
- For enumerated choices, use `MCP::Elicitation::EnumSchema` to construct the canonical schema shapes per
2050
- [SEP-1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330) instead of building
2051
- the underlying Hash by hand. The five class methods cover titled and untitled, single-select and multi-select,
2052
- plus the legacy `enumNames` form retained for backward compatibility:
2053
-
2054
- ```ruby
2055
- size_schema = MCP::Elicitation::EnumSchema.titled_single_select(
2056
- options: [
2057
- { value: "s", title: "Small" },
2058
- { value: "m", title: "Medium" },
2059
- { value: "l", title: "Large" },
2060
- ],
2061
- default: "m",
2062
- )
2063
-
2064
- tags_schema = MCP::Elicitation::EnumSchema.untitled_multi_select(
2065
- values: ["urgent", "billing", "feedback"],
2066
- )
2067
-
2068
- result = server_context.create_form_elicitation(
2069
- message: "Tell us about your order",
2070
- requested_schema: {
2071
- type: "object",
2072
- properties: {
2073
- size: size_schema.to_h,
2074
- tags: tags_schema.to_h,
2075
- },
2076
- required: ["size"],
2077
- },
2078
- )
2079
- ```
2080
-
2081
- The available builders are `untitled_single_select`, `titled_single_select`, `untitled_multi_select`, `titled_multi_select`,
2082
- and `legacy_titled`. Each accepts optional `default:`, `title:`, and `description:`.
2083
-
2084
- The same builders produce the `requestedSchema` of an `elicitation/create` request embedded in a SEP-2322 `input_required` result,
2085
- which is how elicitation reaches clients on the stateless 2026-07-28 lifecycle:
2086
-
2087
- ```ruby
2088
- MCP::Server::InputRequiredResult.new(
2089
- input_requests: {
2090
- "size" => {
2091
- method: "elicitation/create",
2092
- params: {
2093
- message: "Pick a size",
2094
- requestedSchema: {
2095
- type: "object",
2096
- properties: { size: size_schema.to_h },
2097
- required: ["size"],
2098
- },
2099
- },
2100
- },
2101
- },
2102
- )
2103
- ```
2104
-
2105
- #### URL Mode
2106
-
2107
- URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
2108
-
2109
- ```ruby
2110
- server.define_tool(name: "authorize_github", description: "Authorize GitHub") do |server_context:|
2111
- elicitation_id = SecureRandom.uuid
2112
-
2113
- result = server_context.create_url_elicitation(
2114
- message: "Please authorize access to your GitHub account",
2115
- url: "https://example.com/oauth/authorize?elicitation_id=#{elicitation_id}",
2116
- elicitation_id: elicitation_id,
2117
- )
2118
-
2119
- server_context.notify_elicitation_complete(elicitation_id: elicitation_id)
2120
-
2121
- MCP::Tool::Response.new([{ type: "text", text: "Authorization complete" }])
2122
- end
2123
- ```
2124
-
2125
- #### URLElicitationRequiredError
2126
-
2127
- When a tool cannot proceed until an out-of-band elicitation is completed, raise `MCP::Server::URLElicitationRequiredError`.
2128
- This returns a JSON-RPC error with code `-32042` to the client:
2129
-
2130
- ```ruby
2131
- server.define_tool(name: "access_github", description: "Access GitHub") do |server_context:|
2132
- raise MCP::Server::URLElicitationRequiredError.new([
2133
- {
2134
- mode: "url",
2135
- elicitationId: SecureRandom.uuid,
2136
- url: "https://example.com/oauth/authorize",
2137
- message: "GitHub authorization is required.",
2138
- },
2139
- ])
2140
- end
2141
- ```
2142
-
2143
- ### Logging
2144
-
2145
- The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
2146
-
2147
- The `notifications/message` notification is used for structured logging between client and server.
2148
-
2149
- #### Log Levels
2150
-
2151
- The SDK supports 8 log levels with increasing severity:
2152
-
2153
- - `debug` - Detailed debugging information
2154
- - `info` - General informational messages
2155
- - `notice` - Normal but significant events
2156
- - `warning` - Warning conditions
2157
- - `error` - Error conditions
2158
- - `critical` - Critical conditions
2159
- - `alert` - Action must be taken immediately
2160
- - `emergency` - System is unusable
2161
-
2162
- #### How Logging Works
2163
-
2164
- 1. **Client Configuration**: The client sends a `logging/setLevel` request to configure the minimum log level
2165
- 2. **Server Filtering**: The server only sends log messages at the configured level or higher severity
2166
- 3. **Notification Delivery**: Log messages are sent as `notifications/message` to the client
2167
-
2168
- For example, if the client sets the level to `"error"` (severity 4), the server will send messages with levels: `error`, `critical`, `alert`, and `emergency`.
2169
-
2170
- For more details, see the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
2171
-
2172
- **Usage Example:**
2173
-
2174
- ```ruby
2175
- server = MCP::Server.new(name: "my_server")
2176
- transport = MCP::Server::Transports::StdioTransport.new(server)
2177
-
2178
- # The client first configures the logging level (on the client side):
2179
- transport.send_request(
2180
- request: {
2181
- jsonrpc: "2.0",
2182
- method: "logging/setLevel",
2183
- params: { level: "info" },
2184
- id: session_id # Unique request ID within the session
2185
- }
2186
- )
2187
-
2188
- # Send log messages at different severity levels
2189
- server.notify_log_message(
2190
- data: { message: "Application started successfully" },
2191
- level: "info"
2192
- )
2193
-
2194
- server.notify_log_message(
2195
- data: { message: "Configuration file not found, using defaults" },
2196
- level: "warning"
2197
- )
2198
-
2199
- server.notify_log_message(
2200
- data: {
2201
- error: "Database connection failed",
2202
- details: { host: "localhost", port: 5432 }
2203
- },
2204
- level: "error",
2205
- logger: "DatabaseLogger" # Optional logger name
2206
- )
2207
- ```
2208
-
2209
- **Key Features:**
2210
-
2211
- - Supports 8 log levels (debug, info, notice, warning, error, critical, alert, emergency) based on https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#log-levels
2212
- - Server has capability `logging` to send log messages
2213
- - Messages are only sent if a transport is configured
2214
- - Messages are filtered based on the client's configured log level
2215
- - If the log level hasn't been set by the client, no messages will be sent
2216
-
2217
- #### Transport Support
2218
-
2219
- - **stdio**: Notifications are sent as JSON-RPC 2.0 messages to stdout
2220
- - **Streamable HTTP**: Notifications are sent as JSON-RPC 2.0 messages over HTTP with streaming (chunked transfer or SSE)
2221
-
2222
- #### Usage Example
2223
-
2224
- ```ruby
2225
- server = MCP::Server.new(name: "my_server")
2226
-
2227
- # Default Streamable HTTP - session oriented
2228
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
2229
-
2230
- # When tools change, notify clients
2231
- server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
2232
- server.notify_tools_list_changed
2233
-
2234
- # When prompts change, notify clients
2235
- server.define_prompt(name: "new_prompt") do |args, server_context:|
2236
- MCP::Prompt::Result.new(messages: [])
2237
- end
2238
- server.notify_prompts_list_changed
2239
-
2240
- # When resources change, notify clients
2241
- server.define_resource(uri: "resource://new", name: "new_resource", mime_type: "text/plain") do
2242
- [MCP::Resource::TextContents.new(uri: "resource://new", mime_type: "text/plain", text: "contents")]
2243
- end
2244
- server.notify_resources_list_changed
2245
- ```
2246
-
2247
- You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
2248
- This mode allows for easy multi-node deployment.
2249
- Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new` (`stateless` defaults to `false`):
2250
-
2251
- ```ruby
2252
- # Stateless Streamable HTTP - session-less
2253
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
2254
- ```
2255
-
2256
- In stateless mode, each POST is fully self-contained per SEP-2567: no `Mcp-Session-Id` is issued or required,
2257
- handlers run against an ephemeral per-request session (so client identity never leaks across requests or onto the shared server),
2258
- and repeated `initialize` requests are permitted. Request-scoped notifications such as progress and log messages are skipped
2259
- (there is no stream to deliver them), while server-to-client requests (`sampling/createMessage`, `roots/list`, `elicitation/create`) raise an error.
2260
-
2261
- You can enable JSON response mode, where the server returns `application/json` instead of `text/event-stream`.
2262
- Set `enable_json_response: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`:
2263
-
2264
- ```ruby
2265
- # JSON response mode
2266
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, enable_json_response: true)
2267
- ```
2268
-
2269
- In JSON response mode, the POST response is a single JSON object, so server-to-client messages
2270
- that need to arrive during request processing are not supported:
2271
- request-scoped notifications (`progress`, `log`) are silently dropped, and all server-to-client requests
2272
- (`sampling/createMessage`, `roots/list`, `elicitation/create`) raise an error.
2273
- Session-scoped standalone notifications (`resources/updated`, `elicitation/complete`) and
2274
- broadcast notifications (`tools/list_changed`, etc.) still flow to clients connected to the GET SSE stream.
2275
- This mode is suitable for simple tool servers that do not need server-initiated requests.
2276
-
2277
- By default, stateful sessions are bounded so an `initialize` flood cannot retain sessions until memory is exhausted:
2278
- they expire after `session_idle_timeout` seconds of inactivity (default 1800, i.e. 30 minutes) and the concurrent
2279
- session count is capped at `max_sessions` (default 10000). A session's idle timer is reset by activity that touches it
2280
- (a GET, or a regular-request POST), and expired sessions are collected by a background reaper roughly once a minute,
2281
- so cleanup lags inactivity by up to that interval. At the cap, the transport first reclaims any already-expired slots
2282
- and then, if still full, rejects a new `initialize` with HTTP 503 (it does not evict an existing session).
2283
-
2284
- ```ruby
2285
- # Tune the limits
2286
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 900, max_sessions: 5000)
2287
-
2288
- # Opt out of expiry and/or the cap (not recommended on internet-facing deployments)
2289
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: nil, max_sessions: nil)
2290
- ```
2291
-
2292
- Stateless mode (`stateless: true`) retains no sessions, so neither limit applies to it.
2293
-
2294
- #### Session Ownership
2295
-
2296
- `StreamableHTTPTransport` issues a random `SecureRandom.uuid` session ID and validates incoming requests by session
2297
- existence and idle timeout only. It does not bind a session to a user, because the transport never receives
2298
- an authenticated identity on its own. A caller that obtains a valid session ID could therefore act on that session,
2299
- so binding a session to a user is the deploying application's responsibility (the MCP spec frames this as a SHOULD).
2300
-
2301
- The primary control is the `session_request_validator`. It is called as `->(request, session_id) { true | false }`
2302
- on every non-`initialize` POST, GET, and DELETE against an existing session (including notification and response POSTs,
2303
- so a stolen session ID cannot, for example, POST `notifications/cancelled` against a victim's request). A falsy return
2304
- rejects the request with HTTP 403. Use it to compare the request's authenticated principal against the one recorded
2305
- when the session was created:
2306
-
2307
- ```ruby
2308
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(
2309
- server,
2310
- session_request_validator: ->(request, session_id) { owns_session?(request, session_id) },
2311
- )
2312
- ```
2313
-
2314
- Without a validator the transport does not enforce ownership. As a limited defense in depth (not authentication),
2315
- it also records the `Origin` header at `initialize` and rejects a later request whose `Origin` differs, but only
2316
- when both are present - a non-browser client that omits `Origin` (e.g. `curl` or a script) is not stopped by this check.
2317
- Enforcing ownership against a determined attacker requires supplying the validator with an authenticated principal.
2318
-
2319
- #### Request Size Limits
2320
-
2321
- `StreamableHTTPTransport` bounds how many bytes a single POST body may allocate, so a peer cannot exhaust memory
2322
- with one oversized message. A body larger than `max_request_bytes` (default 4 MiB) is rejected with HTTP 413,
2323
- and JSON nesting depth is capped. The 4 MiB default comfortably fits a typical JSON-RPC message (a 4 MiB JSON
2324
- string decodes to roughly 3 MiB of base64 payload) and matches the TypeScript SDK's 4 MB default; raise it only
2325
- if you exchange unusually large payloads:
2326
-
2327
- ```ruby
2328
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, max_request_bytes: 8 * 1024 * 1024)
2329
- ```
2330
-
2331
- ### Pagination
2332
-
2333
- The MCP Ruby SDK supports [pagination](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination)
2334
- for list operations that may return large result sets. Pagination uses string cursor tokens carrying a zero-based offset,
2335
- treated as opaque by clients: the server decides page size, and the client follows `nextCursor` until the server omits it.
2336
-
2337
- Pagination applies to `tools/list`, `prompts/list`, `resources/list`, and `resources/templates/list`.
2338
-
2339
- #### Server-Side: Enabling Pagination
2340
-
2341
- Pass `page_size:` to `MCP::Server.new` to split list responses into pages. When `page_size` is omitted (the default),
2342
- list responses contain all items in a single response, preserving the pre-pagination behavior.
2343
-
2344
- ```ruby
2345
- server = MCP::Server.new(
2346
- name: "my_server",
2347
- tools: tools,
2348
- page_size: 50,
2349
- )
2350
- ```
2351
-
2352
- When `page_size` is set, list responses include a `nextCursor` field whenever more pages are available:
2353
-
2354
- ```json
2355
- {
2356
- "jsonrpc": "2.0",
2357
- "id": 1,
2358
- "result": {
2359
- "tools": [
2360
- { "name": "example_tool" }
2361
- ],
2362
- "nextCursor": "50"
2363
- }
2364
- }
2365
- ```
2366
-
2367
- Invalid cursors (e.g. non-numeric, negative, or out-of-range) are rejected with JSON-RPC error code `-32602 (Invalid params)` per the MCP specification.
2368
-
2369
- #### Client-Side: Iterating Pages
2370
-
2371
- `MCP::Client` exposes `list_tools`, `list_prompts`, `list_resources`, and `list_resource_templates`.
2372
- **Each call issues exactly one `*/list` JSON-RPC request and returns exactly one page** — not the full collection.
2373
- The returned result object (`MCP::Client::ListToolsResult` etc.) exposes the page items and the next cursor as method accessors:
2374
-
2375
- ```ruby
2376
- client = MCP::Client.new(transport: transport)
2377
-
2378
- cursor = nil
2379
- loop do
2380
- page = client.list_tools(cursor: cursor)
2381
- page.tools.each { |tool| process(tool) }
2382
- cursor = page.next_cursor
2383
- break unless cursor
2384
- end
2385
- ```
2386
-
2387
- The same pattern applies to `list_prompts` (`page.prompts`), `list_resources` (`page.resources`), and
2388
- `list_resource_templates` (`page.resource_templates`). `next_cursor` is `nil` on the final page.
2389
-
2390
- Because a single call returns a single page, how many items come back depends on the server's `page_size` configuration:
2391
-
2392
- | Server `page_size` | `client.list_tools(cursor: nil)` |
2393
- |--------------------|---------------------------------------------------------------------|
2394
- | Not set (default) | Returns every item in one response. `next_cursor` is `nil`. |
2395
- | Set to `N` | Returns the first `N` items. `next_cursor` is set for continuation. |
2396
-
2397
- If your application needs the complete collection regardless of how the server is configured, either loop on
2398
- `next_cursor` as shown above, or use the whole-collection methods described below.
2399
-
2400
- #### Fetching the Complete Collection
2401
-
2402
- `client.tools`, `client.resources`, `client.resource_templates`, and `client.prompts` auto-iterate
2403
- through all pages and return a plain array of items, guaranteeing the full collection regardless
2404
- of the server's `page_size` setting. When a server paginates, they issue multiple JSON-RPC round
2405
- trips per call. Two guards keep that loop finite: it stops when the server returns a `nextCursor`
2406
- it has already sent, and it stops after `max_pages` pages.
2407
-
2408
- ```ruby
2409
- tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
2410
- ```
2411
-
2412
- `MCP::Client.new` accepts an optional `max_pages:` keyword that caps how many pages these methods
2413
- will walk. It defaults to `1_000`; a server that keeps offering a fresh `nextCursor` past that
2414
- point raises `MCP::Client::PaginationLimitError` rather than being followed indefinitely. Raise it
2415
- if you legitimately expect more pages than that.
2416
-
2417
- Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
2418
- fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
2419
-
2420
- #### List Result Caching (`ttlMs` / `cacheScope`)
2421
-
2422
- 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;
2423
- `0` means do not cache) and whether shared intermediaries may cache it (`cacheScope`: `"public"` or `"private"`).
2424
-
2425
- 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`,
2426
- `resources/templates/list`, and `resources/read` results (a missing field is filled with the defaults `ttlMs: 0` / `cacheScope: "private"`,
2427
- the scope that keeps a potentially user-dependent result out of shared caches).
2428
- When neither is set, responses are serialized exactly as before.
2429
- The 2026-07-28 revision makes both hints required on these results, so on requests carrying the modern `_meta` envelope
2430
- the server always emits them, filling unset values with the same defaults; stable protocol versions keep the opt-in behavior.
2431
-
2432
- ```ruby
2433
- server = MCP::Server.new(
2434
- name: "my_server",
2435
- tools: tools,
2436
- ttl_ms: 60_000, # results stay fresh for one minute
2437
- cache_scope: "private", # only the requesting client may cache them
2438
- )
2439
- ```
2440
-
2441
- A `resources_read_handler` can override the hints per result by returning a full result hash instead of bare contents:
2442
-
2443
- ```ruby
2444
- server.resources_read_handler do |params|
2445
- { contents: [{ uri: params[:uri], mimeType: "text/plain", text: "..." }], ttlMs: 5_000 }
2446
- end
2447
- ```
2448
-
2449
- On the client, the values are surfaced on the paginated result structs as `ttl_ms` and `cache_scope`:
2450
-
2451
- ```ruby
2452
- page = client.list_tools
2453
- page.ttl_ms # => 60000 (nil when the server sent no hint)
2454
- page.cache_scope # => "private"
2455
- ```
2456
-
2457
- ### Advanced
2458
-
2459
- #### Custom Methods
2460
-
2461
- The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
2462
-
2463
- ```ruby
2464
- server = MCP::Server.new(name: "my_server")
2465
-
2466
- # Define a custom method that returns a result
2467
- server.define_custom_method(method_name: "add") do |params|
2468
- params[:a] + params[:b]
2469
- end
2470
-
2471
- # Define a custom notification method (returns nil)
2472
- server.define_custom_method(method_name: "notify") do |params|
2473
- # Process notification
2474
- nil
2475
- end
2476
- ```
2477
-
2478
- **Key Features:**
2479
-
2480
- - Accepts any method name as a string
2481
- - Block receives the request parameters as a hash
2482
- - Can handle both regular methods (with responses) and notifications
2483
- - Prevents overriding existing MCP protocol methods
2484
- - Supports instrumentation callbacks for monitoring
2485
-
2486
- **Usage Example:**
2487
-
2488
- ```ruby
2489
- # Client request
2490
- {
2491
- "jsonrpc": "2.0",
2492
- "id": 1,
2493
- "method": "add",
2494
- "params": { "a": 5, "b": 3 }
2495
- }
2496
-
2497
- # Server response
2498
- {
2499
- "jsonrpc": "2.0",
2500
- "id": 1,
2501
- "result": 8
2502
- }
2503
- ```
2504
-
2505
- **Error Handling:**
2506
-
2507
- - Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
2508
- - Supports the same exception reporting and instrumentation as standard methods
2509
-
2510
- ## Building an MCP Client
2511
-
2512
- The `MCP::Client` class provides an interface for interacting with MCP servers.
2513
-
2514
- This class supports:
2515
-
2516
- - Liveness check via the `ping` method (`MCP::Client#ping`)
2517
- - Tool listing via the `tools/list` method (`MCP::Client#tools`)
2518
- - Tool invocation via the `tools/call` method (`MCP::Client#call_tool`)
2519
- - Resource listing via the `resources/list` method (`MCP::Client#resources`)
2520
- - Resource template listing via the `resources/templates/list` method (`MCP::Client#resource_templates`)
2521
- - Resource reading via the `resources/read` method (`MCP::Client#read_resource`)
2522
- - Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
2523
- - Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
2524
- - Completion requests via the `completion/complete` method (`MCP::Client#complete`)
2525
- - Automatic JSON-RPC 2.0 message formatting
2526
- - UUID request ID generation
2527
-
2528
- Clients are initialized with a transport layer instance that handles the low-level communication mechanics.
2529
- Authorization is handled by the transport layer.
2530
-
2531
- ### Lifecycle Negotiation (SEP-2575)
2532
-
2533
- `MCP::Client#connect` selects the protocol lifecycle automatically by default: on the bundled
2534
- `MCP::Client::HTTP` and `MCP::Client::Stdio` transports it probes `server/discover` first and adopts
2535
- the stateless modern lifecycle (MCP 2026-07-28) when the server serves it, falling back to
2536
- the classic `initialize` handshake otherwise. Custom transports whose `connect` does not declare
2537
- a `mode:` keyword always receive the classic call shape, unchanged.
2538
-
2539
- ```ruby
2540
- client.connect # negotiate automatically (default)
2541
- client.connect(mode: :legacy) # force the classic initialize handshake
2542
- client.connect(mode: :modern) # require the modern lifecycle; fails on legacy-only servers
2543
- client.connect(protocol_version: "2025-11-25") # an explicit legacy version pins the handshake, no probe
2544
- ```
2545
-
2546
- Prefer `mode: :legacy` for spawn-per-invocation CLI tools (the probe adds a round trip per process)
2547
- and when using server-initiated requests (`on_elicitation` / `on_sampling`), which exist only on
2548
- the legacy lifecycle.
2549
-
2550
- Because the raw `connect` return value and `MCP::Client#server_info` mirror the wire result,
2551
- their shape depends on the negotiated lifecycle: `InitializeResult` (`protocolVersion`,
2552
- top-level `serverInfo`) on legacy, `DiscoverResult` (`supportedVersions`, `ttlMs`/`cacheScope`)
2553
- on modern. Code that should work against both lifecycles can use the era-independent readers instead:
2554
-
2555
- ```ruby
2556
- client.protocol_version # negotiated or adopted version, either lifecycle
2557
- client.server_capabilities # capabilities Hash, either lifecycle
2558
- client.instructions # instructions text, either lifecycle
2559
- client.server_implementation # server name/version; nil when a modern server does not identify itself
2560
- ```
2561
-
2562
- Troubleshooting: if `server_info["protocolVersion"]` starts returning `nil` after a server you connect to was upgraded,
2563
- the server now serves the modern lifecycle and the automatic negotiation adopted it.
2564
- Pass `mode: :legacy` for an immediate return to the previous behavior, or switch to the readers above for a permanent fix.
2565
-
2566
- ### Custom Headers from Tool Parameters (SEP-2243)
2567
-
2568
- On a modern `MCP::Client::HTTP` connection, `tools/call` mirrors arguments whose `inputSchema` property carries
2569
- an `x-mcp-header` annotation into `Mcp-Param-{Name}` request headers, so intermediaries can route
2570
- on the values without parsing bodies. The declarations are learned from `tools/list` responses:
2571
- list the tools before calling one to enable the mirroring. Values that cannot ride as plain ASCII header values
2572
- (non-ASCII, control characters, edge whitespace, empty strings) are wrapped as `=?base64?...?=`,
2573
- and a `null` or absent argument omits its header.
2574
-
2575
- Per the specification, a tool definition whose `x-mcp-header` annotations are invalid (empty or non-token names,
2576
- duplicate names, non-primitive properties, annotations outside a chain of `properties` keys) is excluded from
2577
- `tools/list` results on modern connections, with a warning naming the tool.
2578
- Legacy connections are unaffected: nothing is learned, mirrored, or excluded.
2579
-
2580
- ## Transport Layer Interface
2581
-
2582
- If the transport layer you need is not included in the gem, you can build and pass your own instances so long as they conform to the following interface:
2583
-
2584
- ```ruby
2585
- class CustomTransport
2586
- # Sends a JSON-RPC request to the server and returns the raw response.
2587
- #
2588
- # @param request [Hash] A complete JSON-RPC request object.
2589
- # https://www.jsonrpc.org/specification#request_object
2590
- # @return [Hash] A hash modeling a JSON-RPC response object.
2591
- # https://www.jsonrpc.org/specification#response_object
2592
- def send_request(request:)
2593
- # Your transport-specific logic here
2594
- # - HTTP: POST to endpoint with JSON body
2595
- # - WebSocket: Send message over WebSocket
2596
- # - stdio: Write to stdout, read from stdin
2597
- # - etc.
2598
- end
2599
- end
2600
- ```
2601
-
2602
- ### Stdio Transport Layer
2603
-
2604
- Use the `MCP::Client::Stdio` transport to interact with MCP servers running as subprocesses over standard input/output.
2605
-
2606
- `MCP::Client::Stdio.new` accepts the following keyword arguments:
2607
-
2608
- | Parameter | Required | Description |
2609
- |---|---|---|
2610
- | `command:` | Yes | The command to spawn the server process (e.g., `"ruby"`, `"bundle"`, `"npx"`). |
2611
- | `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
2612
- | `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
2613
- | `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
2614
- | `max_line_bytes:` | No | Maximum byte length of a single newline-delimited response frame. A frame that reaches this limit without a newline is rejected as a transport error, preventing unbounded memory growth from a server that never emits a newline. Defaults to `4 * 1024 * 1024` (4 MiB). |
2615
-
2616
- Example usage:
2617
-
2618
- ```ruby
2619
- stdio_transport = MCP::Client::Stdio.new(
2620
- command: "bundle",
2621
- args: ["exec", "ruby", "path/to/server.rb"],
2622
- env: { "API_KEY" => "my_secret_key" },
2623
- read_timeout: 30
2624
- )
2625
- client = MCP::Client.new(transport: stdio_transport)
2626
-
2627
- # Perform the MCP initialization handshake before sending any requests.
2628
- client.connect
2629
-
2630
- # List available tools.
2631
- tools = client.tools
2632
- tools.each do |tool|
2633
- puts "Tool: #{tool.name} - #{tool.description}"
2634
- end
2635
-
2636
- # Call a specific tool.
2637
- response = client.call_tool(
2638
- tool: tools.first,
2639
- arguments: { message: "Hello, world!" }
2640
- )
2641
-
2642
- # Close the transport when done.
2643
- stdio_transport.close
2644
- ```
2645
-
2646
- The stdio transport automatically handles:
2647
-
2648
- - Spawning the server process with `Open3.popen3`
2649
- - MCP protocol initialization handshake (`initialize` request + `notifications/initialized`)
2650
- - JSON-RPC 2.0 message framing over newline-delimited JSON
2651
-
2652
- ### HTTP Transport Layer
2653
-
2654
- Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
2655
-
2656
- You'll need to add `faraday` as a dependency in order to use the HTTP transport layer. Add `event_stream_parser` as well if the server uses SSE (`text/event-stream`) responses:
2657
-
2658
- ```ruby
2659
- gem 'mcp'
2660
- gem 'faraday', '>= 2.0'
2661
- gem 'event_stream_parser', '>= 1.0' # optional, required only for SSE responses
2662
- ```
2663
-
2664
- Example usage:
2665
-
2666
- ```ruby
2667
- http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
2668
- client = MCP::Client.new(transport: http_transport)
2669
-
2670
- # Perform the MCP initialization handshake before sending any requests.
2671
- client.connect
2672
-
2673
- # List available tools
2674
- tools = client.tools
2675
- tools.each do |tool|
2676
- puts <<~TOOL_INFORMATION
2677
- Tool: #{tool.name}
2678
- Description: #{tool.description}
2679
- Input Schema: #{tool.input_schema}
2680
- TOOL_INFORMATION
2681
- end
2682
-
2683
- # Call a specific tool
2684
- response = client.call_tool(
2685
- tool: tools.first,
2686
- arguments: { message: "Hello, world!" }
2687
- )
2688
-
2689
- # Call a tool with progress tracking.
2690
- response = client.call_tool(
2691
- tool: tools.first,
2692
- arguments: { count: 10 },
2693
- progress_token: "my-progress-token"
2694
- )
2695
- ```
2696
-
2697
- The server will send `notifications/progress` back to the client during execution.
2698
-
2699
- `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 -
2700
- 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
2701
- a server that never terminates an SSE event. It defaults to `4 * 1024 * 1024` (4 MiB); raise it if your server returns larger responses.
2702
-
2703
- `MCP::Client::HTTP.new` also accepts `max_reconnection_wait:`, a budget in seconds for resuming a closed SSE stream. It gates every wait between reconnection attempts,
2704
- and what is left of it becomes the read timeout of each resumed stream. The server chooses that wait through the SSE `retry:` field, and resuming happens on the calling thread,
2705
- so without a budget a server answering with a large `retry:` parks a thread of your application for as long as it likes. It defaults to `300` (5 minutes).
2706
- The server's `retry:` is never shortened: when honoring it would run past the budget, the client stops trying to resume and raises instead,
2707
- the same thing it already does once the reconnection attempts are used up. A floor of 100ms applies to each wait, so a `retry: 0` cannot spin
2708
- the listening stream's reconnect loop; waiting longer than the server asked for is explicitly allowed by the SSE reconnection algorithm the spec points at.
2709
-
2710
- #### Server-to-Client Requests (Elicitation)
2711
-
2712
- Servers can send requests back to the client while one of the client's own requests is in flight - for example,
2713
- [`elicitation/create`](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) to ask the user for additional input during a tool call.
2714
- Register a handler and advertise the capability on `connect` to respond to them:
2715
-
2716
- ```ruby
2717
- client.connect(capabilities: { elicitation: {} })
2718
-
2719
- client.on_elicitation do |params|
2720
- {
2721
- action: "accept",
2722
- # Fill fields omitted by the user with the schema's `default` values (SEP-1034)
2723
- content: MCP::Client::Elicitation.apply_defaults(params["requestedSchema"]),
2724
- }
2725
- end
2726
- ```
2727
-
2728
- Registering a handler opens a standalone HTTP GET SSE stream on a background thread
2729
- ([listening for messages from the server](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server)),
2730
- since servers deliver requests that are not tied to a client request on that stream. Server requests with no registered handler are answered with
2731
- a JSON-RPC `-32601` (method not found) error. To handle methods other than `elicitation/create`, register directly on the transport with
2732
- `http_transport.on_server_request("method/name") { |params| ... }`.
2733
-
2734
- #### Server-to-Client Requests (Sampling)
2735
-
2736
- Servers can also request an LLM completion from the client with [`sampling/createMessage`](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling),
2737
- letting a server leverage the client's model access without its own API keys.
2738
-
2739
- > MCP Sampling is deprecated as of protocol version `2026-07-28` (SEP-2577), while remaining fully supported under `2025-11-25`.
2740
- > Register this handler to interoperate with servers that still send sampling requests during the deprecation window;
2741
- > new servers should call LLM provider APIs directly.
2742
-
2743
- Register a handler and advertise the capability on `connect`:
2744
-
2745
- ```ruby
2746
- client.connect(capabilities: { sampling: {} })
2747
-
2748
- client.on_sampling do |params|
2749
- completion = my_llm.complete(params["messages"], max_tokens: params["maxTokens"])
2750
- {
2751
- role: "assistant",
2752
- content: { type: "text", text: completion.text },
2753
- model: completion.model,
2754
- stopReason: "endTurn",
2755
- }
2756
- end
2757
- ```
2758
-
2759
- For trust and safety, the spec recommends a human in the loop able to review, edit, or reject the request and the generated response.
2760
- To reject a request, raise `MCP::Client::ServerRequestError` with the spec's user-rejection code `-1`:
2761
-
2762
- ```ruby
2763
- client.on_sampling do |params|
2764
- raise MCP::Client::ServerRequestError.new("User rejected sampling request", code: -1) unless approved?(params)
2765
-
2766
- generate_completion(params)
2767
- end
2768
- ```
2769
-
2770
- Use `capabilities: { sampling: { tools: {} } }` to receive tool-enabled sampling requests. Like elicitation, this uses the same standalone GET SSE listening stream.
2771
-
2772
- #### HTTP Authorization
2773
-
2774
- 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:
2775
-
2776
- ```ruby
2777
- http_transport = MCP::Client::HTTP.new(
2778
- url: "https://api.example.com/mcp",
2779
- headers: {
2780
- "Authorization" => "Bearer my_token"
2781
- }
2782
- )
2783
-
2784
- client = MCP::Client.new(transport: http_transport)
2785
- client.tools # will make the call using Bearer auth
2786
- ```
2787
-
2788
- You can add any custom headers needed for your authentication scheme, or for any other purpose. The client will include these headers on every request.
2789
-
2790
- #### OAuth 2.1 Authorization
2791
-
2792
- When an MCP server enforces the [MCP Authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization),
2793
- pass an `MCP::Client::OAuth::Provider` to the transport instead of a static `Authorization` header. The transport will:
2794
-
2795
- - Send `Authorization: Bearer <access_token>` on every request when a token is available.
2796
- - On a `401 Unauthorized`, parse the `WWW-Authenticate` header, discover the authorization server (Protected Resource Metadata + RFC 8414 Authorization Server Metadata),
2797
- perform Dynamic Client Registration if needed, run the OAuth 2.1 Authorization Code flow with PKCE (S256), and retry the failed request with the acquired token.
2798
- - Fall back to the legacy 2025-03-26 discovery when the server publishes no Protected Resource Metadata, matching the TypeScript and Python SDKs: the MCP server's origin acts
2799
- as the authorization base URL, its metadata is fetched from `<origin>/.well-known/oauth-authorization-server` without the RFC 8414 issuer byte-match (which the legacy spec predates),
2800
- and when even that is absent the spec's default endpoints `/authorize`, `/token`, and `/register` at the origin are used with PKCE S256 assumed.
2801
- - On subsequent 401s with a saved `refresh_token`, exchange it at the token endpoint before falling back to the full interactive flow (RFC 6749 Section 6).
2802
- - On a `403 Forbidden` whose `WWW-Authenticate` header carries `error="insufficient_scope"` (OAuth 2.0 step-up, RFC 6750 Section 3.1 and the MCP scope-selection-strategy),
2803
- run a fresh authorization request for the union of the currently granted scope and the scope named in the challenge, then retry the failed request once.
2804
- The refresh path is bypassed because refreshing would re-issue the same scope set the server just rejected. A `403` without that challenge is surfaced unchanged.
2805
- - Request the `offline_access` scope when `client_metadata[:grant_types]` includes `refresh_token` and the authorization server advertises `offline_access` in its metadata
2806
- `scopes_supported` (SEP-2207). This is what lets the server issue the `refresh_token` used above. As an SDK-level safeguard, when the authorization server does not advertise
2807
- `offline_access` the scope is also stripped from any other source (challenge, PRM, or provider-supplied scope) so a server that does not support it never receives it.
2808
-
2809
- ```ruby
2810
- require "mcp"
2811
-
2812
- provider = MCP::Client::OAuth::Provider.new(
2813
- client_metadata: {
2814
- client_name: "My MCP App",
2815
- redirect_uris: ["http://localhost:3030/callback"],
2816
- grant_types: ["authorization_code", "refresh_token"],
2817
- response_types: ["code"],
2818
- token_endpoint_auth_method: "none",
2819
- },
2820
- redirect_uri: "http://localhost:3030/callback",
2821
- redirect_handler: ->(authorization_url) {
2822
- # Send the user to the authorization URL - typically `Launchy.open(authorization_url)`
2823
- # or a manual `puts authorization_url` in CLI tools.
2824
- },
2825
- callback_handler: -> {
2826
- # Capture the redirect (for example, by running a small HTTP listener on
2827
- # `redirect_uri`) and return [code, state] from the query string.
2828
- },
2829
- )
2830
-
2831
- transport = MCP::Client::HTTP.new(
2832
- url: "https://api.example.com/mcp",
2833
- oauth: provider,
2834
- )
2835
- client = MCP::Client.new(transport: transport)
2836
- client.connect # `initialize` is sent here; if the server replies 401 the OAuth flow runs and the handshake is retried with the acquired token
2837
- client.tools
2838
- ```
2839
-
2840
- Required keyword arguments to `Provider.new`:
2841
-
2842
- - `client_metadata`: Hash sent to the authorization server's Dynamic Client Registration endpoint. Must include `redirect_uris`, `grant_types`, `response_types`,
2843
- `token_endpoint_auth_method`. `redirect_uri` (below) must appear in this list, otherwise the constructor raises `Provider::UnregisteredRedirectURIError`.
2844
- When `application_type` is omitted, the SDK infers `"native"` or `"web"` from `redirect_uris` per SEP-837 before registering (loopback or custom-scheme URIs are native);
2845
- an explicit value always wins.
2846
- - `redirect_uri`: String. Must use HTTPS or be a loopback URL (`localhost`, `127.0.0.0/8`, `::1`); other values raise `Provider::InsecureRedirectURIError`.
2847
- - `redirect_handler`: Callable invoked with the fully-built authorization `URI`. Typically opens the user's browser.
2848
- - `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
2849
- (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
2850
- the authorization server's issuer, and a missing one is rejected when the server advertises `authorization_response_iss_parameter_supported`.
2851
-
2852
- Optional keyword arguments:
2853
-
2854
- - `scope`: Space-separated scopes to request when the server's `WWW-Authenticate` does not specify one.
2855
- - `storage`: Object responding to `tokens`, `save_tokens(t)`, `client_information`, `save_client_information(info)`. Defaults to `MCP::Client::OAuth::InMemoryStorage`,
2856
- which keeps credentials in process memory only. Persisted `client_information` is stamped with an `"issuer"` member binding it to the authorization server that
2857
- issued it (SEP-2352): when the server's authorization server changes, the SDK discards the stale registration and its tokens and re-registers automatically
2858
- (portable CIMD `client_id`s are kept). Treat the hash as opaque and persist it as-is.
2859
- - `client_id_metadata_document_url`: URL where you publish a Client ID Metadata Document
2860
- (`draft-ietf-oauth-client-id-metadata-document` and the MCP authorization specification).
2861
- When the authorization server advertises `client_id_metadata_document_supported: true`,
2862
- the SDK uses this URL as the OAuth `client_id` and skips Dynamic Client Registration.
2863
- Spec-required: the URL MUST be `https://` with a non-root path and MUST NOT include a fragment,
2864
- userinfo, or `.`/`..` segments. The SDK additionally rejects query strings (the draft only marks
2865
- them SHOULD NOT include, but the SDK refuses to send any) for `client_id` stability.
2866
- Any of these failures raise `Provider::InvalidClientIDMetadataDocumentURLError`. The CIMD document
2867
- served at the URL is a separate JSON artifact from the `client_metadata` keyword above:
2868
- the DCR `client_metadata` MUST NOT include `client_id`, while the CIMD document MUST include
2869
- `client_id` set to the document URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
2870
-
2871
- To persist credentials across restarts, supply your own storage:
2872
-
2873
- ```ruby
2874
- class FileTokenStorage
2875
- def initialize(path)
2876
- @path = path
2877
- end
2878
-
2879
- def tokens
2880
- read["tokens"]
2881
- end
2882
-
2883
- def save_tokens(value)
2884
- write("tokens" => value)
2885
- end
2886
-
2887
- def client_information
2888
- read["client"]
2889
- end
2890
-
2891
- def save_client_information(value)
2892
- write("client" => value)
2893
- end
2894
-
2895
- private
2896
-
2897
- def read
2898
- File.exist?(@path) ? JSON.parse(File.read(@path)) : {}
2899
- end
2900
-
2901
- def write(updates)
2902
- File.write(@path, JSON.dump(read.merge(updates)))
2903
- end
2904
- end
2905
-
2906
- provider = MCP::Client::OAuth::Provider.new(
2907
- # ... required keywords ...
2908
- storage: FileTokenStorage.new(File.expand_path("~/.config/my-app/oauth.json")),
2909
- )
2910
- ```
2911
-
2912
- ##### Client Credentials Grant
2913
-
2914
- For a confidential machine-to-machine client (no user, no browser redirect), use `MCP::Client::OAuth::ClientCredentialsProvider` instead of `Provider`.
2915
- The transport discovers the authorization server the same way, then exchanges the OAuth 2.1 `client_credentials` grant (RFC 6749 Section 4.4) at
2916
- the token endpoint. There is no authorization request, PKCE, or `offline_access`, because the grant does not issue a refresh token.
2917
-
2918
- ```ruby
2919
- provider = MCP::Client::OAuth::ClientCredentialsProvider.new(
2920
- client_id: "my-service",
2921
- client_secret: ENV.fetch("MCP_CLIENT_SECRET"),
2922
- # token_endpoint_auth_method: "client_secret_basic" (default) or "client_secret_post"
2923
- # scope: "mcp:read mcp:write" (optional; used when the server does not advertise scopes)
2924
- )
2925
-
2926
- transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp", oauth: provider)
2927
- ```
2928
-
2929
- Keyword arguments:
2930
-
2931
- - `client_id`, `client_secret`: Required. The grant is for confidential clients, so a credential is mandatory.
2932
- - `token_endpoint_auth_method`: `"client_secret_basic"` (default) or `"client_secret_post"`. `"none"` is rejected with `ClientCredentialsProvider::InvalidCredentialsError`.
2933
- - `scope`, `storage`: Optional, same meaning as on `Provider`.
2934
-
2935
- ##### Cross-App Access (JWT Bearer) Grant
2936
-
2937
- For enterprise MCP deployments where an identity provider (IdP) governs authorization (SEP-990), use `MCP::Client::OAuth::CrossAppAccessProvider` instead of `Provider`.
2938
- 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
2939
- 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`.
2940
- Mirrors `CrossAppAccessProvider` and `requestJwtAuthorizationGrant` in the TypeScript SDK.
2941
-
2942
- `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
2943
- an enterprise secret store or a test double without changing the transport wiring.
2944
-
2945
- ```ruby
2946
- provider = MCP::Client::OAuth::CrossAppAccessProvider.new(
2947
- client_id: "my-mcp-client",
2948
- client_secret: ENV.fetch("MCP_CLIENT_SECRET"),
2949
- assertion_provider: ->(audience:, resource:) {
2950
- MCP::Client::OAuth::IDJAGTokenExchange.request(
2951
- token_endpoint: "https://idp.example.com/token",
2952
- id_token: ENV.fetch("IDP_ID_TOKEN"),
2953
- client_id: "my-idp-client",
2954
- audience: audience,
2955
- resource: resource,
2956
- )
2957
- },
2958
- # scope: "mcp:read mcp:write" (optional; used when neither WWW-Authenticate nor PRM specify one)
2959
- )
2960
-
2961
- transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp", oauth: provider)
2962
- ```
2963
-
2964
- Keyword arguments:
2965
-
2966
- - `client_id`, `client_secret`: Required. The `jwt-bearer` grant authenticates with `client_secret_basic` at the MCP authorization server.
2967
- - `assertion_provider`: Required. Callable invoked as `call(audience:, resource:)` and returning the ID-JAG assertion.
2968
- `audience` is the MCP authorization server's validated issuer identifier; `resource` is the canonical MCP server URL (RFC 8707).
2969
- Passing both through to `IDJAGTokenExchange.request` covers the common case.
2970
- - `scope`, `storage`: Optional, same meaning as on `Provider`.
2971
-
2972
- ##### Communication Security
2973
-
2974
- When `oauth:` is set, the MCP transport URL and every OAuth-facing URL (PRM, Authorization Server metadata, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`,
2975
- `redirect_uri`) must use HTTPS or a loopback host. Non-loopback `http://` URLs are rejected at the SDK boundary so a bearer token is never sent over plain HTTP to a remote host.
2976
-
2977
- The transport also snapshots the canonicalized origin, path, and query string of the MCP URL at `initialize` time and re-checks them on every outgoing request through
2978
- a Faraday middleware that runs after any user-supplied customizer. That means any URL swap raises `MCP::Client::HTTP::InsecureURLError` before the request reaches the adapter,
2979
- whether the swap was triggered by
2980
- `instance_variable_set(:@url, ...)`, by a Faraday customizer rewriting `url_prefix`, or by a custom middleware rewriting `env.url` (including just `env.url.query`) at request time,
2981
- and whether the new URL is `http://` *or* `https://` to a different host or tenant.
2982
-
2983
- ##### Discovery URL Destinations
2984
-
2985
- The scheme rules above say how a URL is contacted, not where it points. Discovery URLs arrive from the network, so the SDK also constrains their destinations.
2986
- Both checks run before the request is sent, and neither is configurable.
2987
-
2988
- - The `resource_metadata` URL in a `WWW-Authenticate` challenge must be on the MCP server's own origin. Protected Resource Metadata describes that server,
2989
- so a real deployment publishes it there; requiring it means a `401` cannot aim the first request of the flow at an unrelated host. This is stricter than RFC 9728,
2990
- which does not require it.
2991
- - The PRM `authorization_servers` entry and the `authorization_endpoint`, `token_endpoint`, and `registration_endpoint` from Authorization Server metadata must not be
2992
- IP literals in a private, loopback, link-local, or unique-local range, per the SSRF precaution in [RFC 9728 Section 7.7](https://www.rfc-editor.org/rfc/rfc9728#section-7.7).
2993
- The blocked ranges are `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`, `::/96`, `fc00::/7`,
2994
- and `fe80::/10`, along with the IPv4-mapped IPv6 spellings of each and the `localhost` name.
2995
- - That range check is skipped when the MCP server URL you configured is itself on such an address. Pointing the client at a private network is a deliberate act,
2996
- and the authorization server for it usually lives on the same network, so `http://localhost` development and deployments that never leave a corporate network keep working.
2997
-
2998
- The range check compares IP literals and does not resolve hostnames, so it cannot recognize an internal service that is named rather than addressed,
2999
- such as `https://vault.corp.internal/`. Resolving names here would not close that gap either, because the address the SDK looked up need not be the one
3000
- the HTTP client connects to a moment later. The same-origin rule is what protects the `resource_metadata` URL, which is the only one of these a server supplies directly.
3001
-
3002
- If you replace the OAuth HTTP client through `MCP::Client::OAuth::Flow.new(http_client_factory:)`, do not add redirect-following middleware. Every check above runs against
3003
- the URL as written, so a connection that follows a `3xx` on its own would reach hosts these rules just refused.
3004
-
3005
- #### Customizing the Faraday Connection
3006
-
3007
- You can pass a block to `MCP::Client::HTTP.new` to customize the underlying Faraday connection.
3008
- The block is called after the default middleware is configured, so you can add middleware or swap the HTTP adapter:
3009
-
3010
- ```ruby
3011
- http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp") do |faraday|
3012
- faraday.use MyApp::Middleware::HttpRecorder
3013
- faraday.adapter :typhoeus
3014
- end
3015
- ```
3016
-
3017
- ### Tool Objects
3018
-
3019
- The client provides a wrapper class for tools returned by the server:
3020
-
3021
- - `MCP::Client::Tool` - Represents a single tool with its metadata
3022
-
3023
- This class provides easy access to tool properties like name, description, input schema, and output schema.
3024
-
3025
- ### Multi-Round-Trip Results (Experimental, SEP-2322)
3026
-
3027
- 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`,
3028
- 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
3029
- and an opaque `requestState`; the client fulfills the requests and re-issues the original request with `inputResponses` and the echoed `requestState`.
3030
-
3031
- 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`,
3032
- 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`
3033
- are provided for forward compatibility. Servers on stable protocol versions never send `resultType`, so existing behavior is unchanged.
3034
-
3035
- SEP-2322 also makes `resultType` a required member of every result a 2026-07-28 server returns. The server stamps `resultType: "complete"` on all results of requests carrying
3036
- the modern `_meta` envelope (and on `server/discover` results), while results that already carry a discriminator (`"input_required"`, the tasks extension's `"task"`) keep it.
3037
- Legacy results stay unstamped, and clients treat an absent `resultType` as `"complete"` per the spec.
3038
-
3039
- #### Dual-era authoring (legacy fulfilment shim)
3040
-
3041
- Handlers written in the 2026 style serve pre-2026 clients too: when a `tools/call`, `prompts/get`, or `resources/read` handler returns an `InputRequiredResult` on the legacy wire,
3042
- the server fulfills it in place of the client's driver. Each `inputRequests` entry is sent as the equivalent real server-to-client request
3043
- (`elicitation/create`, `sampling/createMessage`, `roots/list`), associated with the originating request per SEP-2260; the answers are collected under the same keys,
3044
- and the handler re-runs with `server_context.input_responses` populated and the raw `requestState` echoed, the same deterministic replay contract the modern client driver follows.
3045
- The shim is on by default (matching the TypeScript SDK) and capped at 8 rounds; `MCP::Server.new(input_required_legacy_shim: false)` restores the strict rejection of `input_required` results on legacy requests.
3046
-
3047
- ## Conformance Testing
3048
-
3049
- 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).
3050
-
3051
- See [conformance/README.md](conformance/README.md) for usage instructions.
3052
-
3053
- ## Documentation
3054
-
3055
- - [SDK API documentation](https://rubydoc.info/gems/mcp)
3056
- - [Model Context Protocol documentation](https://modelcontextprotocol.io)
139
+ This project is licensed under the Apache License 2.0 for new contributions, with existing code under MIT. See the [LICENSE](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/LICENSE) file for details.