mcp 0.10.0 → 0.15.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
@@ -38,6 +38,10 @@ It implements the Model Context Protocol specification, handling model context r
38
38
  - Supports resource registration and retrieval
39
39
  - Supports stdio & Streamable HTTP (including SSE) transports
40
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 server-side cancellation of in-flight requests (notifications/cancelled)
41
45
 
42
46
  ### Supported Methods
43
47
 
@@ -50,1012 +54,1682 @@ It implements the Model Context Protocol specification, handling model context r
50
54
  - `resources/list` - Lists all registered resources and their schemas
51
55
  - `resources/read` - Retrieves a specific resource by name
52
56
  - `resources/templates/list` - Lists all registered resource templates and their schemas
57
+ - `resources/subscribe` - Subscribes to updates for a specific resource
58
+ - `resources/unsubscribe` - Unsubscribes from updates for a specific resource
59
+ - `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
60
+ - `roots/list` - Requests filesystem roots from the client (server-to-client)
61
+ - `sampling/createMessage` - Requests LLM completion from the client (server-to-client)
62
+ - `elicitation/create` - Requests user input from the client (server-to-client)
53
63
 
54
- ### Custom Methods
64
+ ### Usage
55
65
 
56
- The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
66
+ #### Stdio Transport
67
+
68
+ If you want to build a local command-line application, you can use the stdio transport:
57
69
 
58
70
  ```ruby
59
- server = MCP::Server.new(name: "my_server")
71
+ require "mcp"
60
72
 
61
- # Define a custom method that returns a result
62
- server.define_custom_method(method_name: "add") do |params|
63
- params[:a] + params[:b]
64
- end
73
+ # Create a simple tool
74
+ class ExampleTool < MCP::Tool
75
+ description "A simple example tool that echoes back its arguments"
76
+ input_schema(
77
+ properties: {
78
+ message: { type: "string" },
79
+ },
80
+ required: ["message"]
81
+ )
65
82
 
66
- # Define a custom notification method (returns nil)
67
- server.define_custom_method(method_name: "notify") do |params|
68
- # Process notification
69
- nil
83
+ class << self
84
+ def call(message:, server_context:)
85
+ MCP::Tool::Response.new([{
86
+ type: "text",
87
+ text: "Hello from example tool! Message: #{message}",
88
+ }])
89
+ end
90
+ end
70
91
  end
92
+
93
+ # Set up the server
94
+ server = MCP::Server.new(
95
+ name: "example_server",
96
+ tools: [ExampleTool],
97
+ )
98
+
99
+ # Create and start the transport
100
+ transport = MCP::Server::Transports::StdioTransport.new(server)
101
+ transport.open
71
102
  ```
72
103
 
73
- **Key Features:**
104
+ You can run this script and then type in requests to the server at the command line.
74
105
 
75
- - Accepts any method name as a string
76
- - Block receives the request parameters as a hash
77
- - Can handle both regular methods (with responses) and notifications
78
- - Prevents overriding existing MCP protocol methods
79
- - Supports instrumentation callbacks for monitoring
106
+ ```console
107
+ $ ruby examples/stdio_server.rb
108
+ {"jsonrpc":"2.0","id":"1","method":"ping"}
109
+ {"jsonrpc":"2.0","id":"2","method":"tools/list"}
110
+ {"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
111
+ ```
80
112
 
81
- **Usage Example:**
113
+ #### Streamable HTTP Transport
114
+
115
+ `MCP::Server::Transports::StreamableHTTPTransport` is a standard Rack app, so it can be mounted in any Rack-compatible framework.
116
+ The following examples show two common integration styles in Rails.
117
+
118
+ > [!IMPORTANT]
119
+ > `MCP::Server::Transports::StreamableHTTPTransport` stores session and SSE stream state in memory,
120
+ > so it must run in a single process. Use a single-process server (e.g., Puma with `workers 0`).
121
+ > Multi-process configurations (Unicorn, or Puma with `workers > 0`) fork separate processes that
122
+ > do not share memory, which breaks session management and SSE connections.
123
+ >
124
+ > When running multiple server instances behind a load balancer, configure your load balancer to use
125
+ > sticky sessions (session affinity) so that requests with the same `Mcp-Session-Id` header are always
126
+ > routed to the same instance.
127
+ >
128
+ > Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
129
+
130
+ ##### Rails (mount)
131
+
132
+ `StreamableHTTPTransport` is a Rack app that can be mounted directly in Rails routes:
82
133
 
83
134
  ```ruby
84
- # Client request
85
- {
86
- "jsonrpc": "2.0",
87
- "id": 1,
88
- "method": "add",
89
- "params": { "a": 5, "b": 3 }
90
- }
135
+ # config/routes.rb
136
+ server = MCP::Server.new(
137
+ name: "my_server",
138
+ title: "Example Server Display Name",
139
+ version: "1.0.0",
140
+ instructions: "Use the tools of this server as a last resort",
141
+ tools: [SomeTool, AnotherTool],
142
+ prompts: [MyPrompt],
143
+ )
144
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
91
145
 
92
- # Server response
93
- {
94
- "jsonrpc": "2.0",
95
- "id": 1,
96
- "result": 8
97
- }
146
+ Rails.application.routes.draw do
147
+ mount transport => "/mcp"
148
+ end
98
149
  ```
99
150
 
100
- **Error Handling:**
101
-
102
- - Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
103
- - Supports the same exception reporting and instrumentation as standard methods
151
+ `mount` directs all HTTP methods on `/mcp` to the transport. `StreamableHTTPTransport` internally dispatches
152
+ `POST` (client-to-server JSON-RPC messages, with responses optionally streamed via SSE),
153
+ `GET` (optional standalone SSE stream for server-to-client messages), and `DELETE` (session termination) per
154
+ the [MCP Streamable HTTP transport spec](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http),
155
+ so no additional route configuration is needed.
104
156
 
105
- ### Notifications
157
+ ##### Rails (controller)
106
158
 
107
- The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
159
+ While the mount approach creates a single server at boot time, the controller approach creates a new server per request.
160
+ This allows you to customize tools, prompts, or configuration based on the request (e.g., different tools per route).
108
161
 
109
- #### Notification Methods
162
+ `StreamableHTTPTransport#handle_request` returns proper HTTP status codes (e.g., 202 Accepted for notifications):
110
163
 
111
- The server provides the following notification methods:
164
+ ```ruby
165
+ class McpController < ActionController::API
166
+ def create
167
+ server = MCP::Server.new(
168
+ name: "my_server",
169
+ title: "Example Server Display Name",
170
+ version: "1.0.0",
171
+ instructions: "Use the tools of this server as a last resort",
172
+ tools: [SomeTool, AnotherTool],
173
+ prompts: [MyPrompt],
174
+ server_context: { user_id: current_user.id },
175
+ )
176
+ # Since the `MCP-Session-Id` is not shared across requests, `stateless: true` is set.
177
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
178
+ status, headers, body = transport.handle_request(request)
112
179
 
113
- - `notify_tools_list_changed` - Send a notification when the tools list changes
114
- - `notify_prompts_list_changed` - Send a notification when the prompts list changes
115
- - `notify_resources_list_changed` - Send a notification when the resources list changes
116
- - `notify_log_message` - Send a structured logging notification message
180
+ render(json: body.first, status: status, headers: headers)
181
+ end
182
+ end
183
+ ```
117
184
 
118
- #### Session Scoping
185
+ ### Configuration
119
186
 
120
- When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
187
+ The gem can be configured using the `MCP.configure` block:
121
188
 
122
- - **`report_progress`** and **`notify_log_message`** called via `server_context` inside a tool handler are automatically sent only to the requesting client.
123
- No extra configuration is needed.
124
- - **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
125
- as they represent server-wide state changes. These should be called on the `server` instance directly.
189
+ ```ruby
190
+ MCP.configure do |config|
191
+ config.exception_reporter = ->(exception, server_context) {
192
+ # Your exception reporting logic here
193
+ # For example with Bugsnag:
194
+ Bugsnag.notify(exception) do |report|
195
+ report.add_metadata(:model_context_protocol, server_context)
196
+ end
197
+ }
126
198
 
127
- #### Notification Format
199
+ config.around_request = ->(data, &request_handler) {
200
+ logger.info("Start: #{data[:method]}")
201
+ request_handler.call
202
+ logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
203
+ }
204
+ end
205
+ ```
128
206
 
129
- Notifications follow the JSON-RPC 2.0 specification and use these method names:
207
+ or by creating an explicit configuration and passing it into the server.
208
+ This is useful for systems where an application hosts more than one MCP server but
209
+ they might require different configurations.
130
210
 
131
- - `notifications/tools/list_changed`
132
- - `notifications/prompts/list_changed`
133
- - `notifications/resources/list_changed`
134
- - `notifications/progress`
135
- - `notifications/message`
211
+ ```ruby
212
+ configuration = MCP::Configuration.new
213
+ configuration.exception_reporter = ->(exception, server_context) {
214
+ # Your exception reporting logic here
215
+ # For example with Bugsnag:
216
+ Bugsnag.notify(exception) do |report|
217
+ report.add_metadata(:model_context_protocol, server_context)
218
+ end
219
+ }
136
220
 
137
- ### Progress
221
+ configuration.around_request = ->(data, &request_handler) {
222
+ logger.info("Start: #{data[:method]}")
223
+ request_handler.call
224
+ logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
225
+ }
138
226
 
139
- The MCP Ruby SDK supports progress tracking for long-running tool operations,
140
- following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
227
+ server = MCP::Server.new(
228
+ # ... all other options
229
+ configuration:,
230
+ )
231
+ ```
141
232
 
142
- #### How Progress Works
233
+ ### Server Context and Configuration Block Data
143
234
 
144
- 1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
145
- 2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
146
- 3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
235
+ #### `server_context`
147
236
 
148
- #### Server-Side: Tool with Progress
237
+ The `server_context` is a user-defined hash that is passed into the server instance and made available to tool and prompt calls.
238
+ It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
149
239
 
150
- Tools that accept a `server_context:` parameter can call `report_progress` on it.
151
- The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
240
+ **Type:**
152
241
 
153
242
  ```ruby
154
- class LongRunningTool < MCP::Tool
155
- description "A tool that reports progress during execution"
156
- input_schema(
157
- properties: {
158
- count: { type: "integer" },
159
- },
160
- required: ["count"]
161
- )
243
+ server_context: { [String, Symbol] => Any }
244
+ ```
162
245
 
163
- def self.call(count:, server_context:)
164
- count.times do |i|
165
- # Do work here.
166
- server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
167
- end
246
+ **Example:**
168
247
 
169
- MCP::Tool::Response.new([{ type: "text", text: "Done" }])
170
- end
171
- end
248
+ ```ruby
249
+ server = MCP::Server.new(
250
+ name: "my_server",
251
+ server_context: { user_id: current_user.id, request_id: request.uuid }
252
+ )
172
253
  ```
173
254
 
174
- The `server_context.report_progress` method accepts:
255
+ This hash is then passed as the `server_context` keyword argument to tool and prompt calls.
256
+ Note that exception and instrumentation callbacks do not receive this user-defined hash.
257
+ See the relevant sections below for the arguments they receive.
175
258
 
176
- - `progress` (required) — current progress value (numeric)
177
- - `total:` (optional) — total expected value, so clients can display a percentage
178
- - `message:` (optional) — human-readable status message
259
+ #### Request-specific `_meta` Parameter
179
260
 
180
- **Key Features:**
261
+ 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`.
181
262
 
182
- - Tools report progress via `server_context.report_progress`
183
- - `report_progress` is a no-op when no `progressToken` was provided by the client
184
- - Supports both numeric and string progress tokens
263
+ **Access Pattern:**
185
264
 
186
- ### Logging
265
+ When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
187
266
 
188
- 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).
267
+ ```ruby
268
+ class MyTool < MCP::Tool
269
+ def self.call(message:, server_context:)
270
+ # Access provider-specific metadata
271
+ session_id = server_context.dig(:_meta, :session_id)
272
+ request_id = server_context.dig(:_meta, :request_id)
189
273
 
190
- The `notifications/message` notification is used for structured logging between client and server.
274
+ # Access server's original context
275
+ user_id = server_context.dig(:user_id)
191
276
 
192
- #### Log Levels
277
+ MCP::Tool::Response.new([{
278
+ type: "text",
279
+ text: "Processing for user #{user_id} in session #{session_id}"
280
+ }])
281
+ end
282
+ end
283
+ ```
193
284
 
194
- The SDK supports 8 log levels with increasing severity:
285
+ **Client Request Example:**
195
286
 
196
- - `debug` - Detailed debugging information
197
- - `info` - General informational messages
198
- - `notice` - Normal but significant events
199
- - `warning` - Warning conditions
200
- - `error` - Error conditions
201
- - `critical` - Critical conditions
202
- - `alert` - Action must be taken immediately
203
- - `emergency` - System is unusable
287
+ ```json
288
+ {
289
+ "jsonrpc": "2.0",
290
+ "id": 1,
291
+ "method": "tools/call",
292
+ "params": {
293
+ "name": "my_tool",
294
+ "arguments": { "message": "Hello" },
295
+ "_meta": {
296
+ "session_id": "abc123",
297
+ "request_id": "req_456"
298
+ }
299
+ }
300
+ }
301
+ ```
204
302
 
205
- #### How Logging Works
303
+ #### Configuration Block Data
206
304
 
207
- 1. **Client Configuration**: The client sends a `logging/setLevel` request to configure the minimum log level
208
- 2. **Server Filtering**: The server only sends log messages at the configured level or higher severity
209
- 3. **Notification Delivery**: Log messages are sent as `notifications/message` to the client
305
+ ##### Exception Reporter
210
306
 
211
- For example, if the client sets the level to `"error"` (severity 4), the server will send messages with levels: `error`, `critical`, `alert`, and `emergency`.
307
+ The exception reporter receives:
212
308
 
213
- For more details, see the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
309
+ - `exception`: The Ruby exception object that was raised
310
+ - `server_context`: A hash describing where the failure occurred (e.g., `{ request: <raw JSON-RPC request> }`
311
+ for request handling, `{ notification: "tools_list_changed" }` for notification delivery).
312
+ This is not the user-defined `server_context` passed to `Server.new`.
214
313
 
215
- **Usage Example:**
314
+ **Signature:**
216
315
 
217
316
  ```ruby
218
- server = MCP::Server.new(name: "my_server")
219
- transport = MCP::Server::Transports::StdioTransport.new(server)
220
- server.transport = transport
317
+ exception_reporter = ->(exception, server_context) { ... }
318
+ ```
221
319
 
222
- # The client first configures the logging level (on the client side):
223
- transport.send_request(
224
- request: {
225
- jsonrpc: "2.0",
226
- method: "logging/setLevel",
227
- params: { level: "info" },
228
- id: session_id # Unique request ID within the session
320
+ ##### Around Request
321
+
322
+ The `around_request` hook wraps request handling, allowing you to execute code before and after each request.
323
+ This is useful for Application Performance Monitoring (APM) tracing, logging, or other observability needs.
324
+
325
+ The hook receives a `data` hash and a `request_handler` block. You must call `request_handler.call` to execute the request:
326
+
327
+ **Signature:**
328
+
329
+ ```ruby
330
+ around_request = ->(data, &request_handler) { request_handler.call }
331
+ ```
332
+
333
+ **`data` availability by timing:**
334
+
335
+ - Before `request_handler.call`: `method`
336
+ - After `request_handler.call`: `tool_name`, `tool_arguments`, `prompt_name`, `resource_uri`, `error`, `client`
337
+ - Not available inside `around_request`: `duration` (added after `around_request` returns)
338
+
339
+ > [!NOTE]
340
+ > `tool_name`, `prompt_name` and `resource_uri` may only be populated for the corresponding request methods
341
+ > (`tools/call`, `prompts/get`, `resources/read`), and may not be set depending on how the request is handled
342
+ > (for example, `prompt_name` is not recorded when the prompt is not found).
343
+ > `duration` is added after `around_request` returns, so it is not visible from within the hook.
344
+
345
+ **Example:**
346
+
347
+ ```ruby
348
+ MCP.configure do |config|
349
+ config.around_request = ->(data, &request_handler) {
350
+ logger.info("Start: #{data[:method]}")
351
+ request_handler.call
352
+ logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
229
353
  }
230
- )
354
+ end
355
+ ```
231
356
 
232
- # Send log messages at different severity levels
233
- server.notify_log_message(
234
- data: { message: "Application started successfully" },
235
- level: "info"
236
- )
357
+ ##### Instrumentation Callback (soft-deprecated)
237
358
 
238
- server.notify_log_message(
239
- data: { message: "Configuration file not found, using defaults" },
240
- level: "warning"
241
- )
359
+ > [!NOTE]
360
+ > `instrumentation_callback` is soft-deprecated. Use `around_request` instead.
361
+ >
362
+ > To migrate, wrap the call in `begin/ensure` so the callback still runs when the request fails:
363
+ >
364
+ > ```ruby
365
+ > # Before
366
+ > config.instrumentation_callback = ->(data) { log(data) }
367
+ >
368
+ > # After
369
+ > config.around_request = ->(data, &request_handler) do
370
+ > request_handler.call
371
+ > ensure
372
+ > log(data)
373
+ > end
374
+ > ```
375
+ >
376
+ > Note that `data[:duration]` is not available inside `around_request`.
377
+ > If you need it, measure elapsed time yourself within the hook, or keep using `instrumentation_callback`.
378
+
379
+ The instrumentation callback is called after each request finishes, whether successfully or with an error.
380
+ It receives a hash with the following possible keys:
242
381
 
243
- server.notify_log_message(
244
- data: {
245
- error: "Database connection failed",
246
- details: { host: "localhost", port: 5432 }
247
- },
248
- level: "error",
249
- logger: "DatabaseLogger" # Optional logger name
250
- )
382
+ - `method`: (String) The protocol method called (e.g., "ping", "tools/list")
383
+ - `tool_name`: (String, optional) The name of the tool called
384
+ - `tool_arguments`: (Hash, optional) The arguments passed to the tool
385
+ - `prompt_name`: (String, optional) The name of the prompt called
386
+ - `resource_uri`: (String, optional) The URI of the resource called
387
+ - `error`: (String, optional) Error code if a lookup failed
388
+ - `duration`: (Float) Duration of the call in seconds
389
+ - `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
390
+
391
+ **Signature:**
392
+
393
+ ```ruby
394
+ instrumentation_callback = ->(data) { ... }
251
395
  ```
252
396
 
253
- **Key Features:**
397
+ ### Server Protocol Version
254
398
 
255
- - 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
256
- - Server has capability `logging` to send log messages
257
- - Messages are only sent if a transport is configured
258
- - Messages are filtered based on the client's configured log level
259
- - If the log level hasn't been set by the client, no messages will be sent
399
+ The server's protocol version can be overridden using the `protocol_version` keyword argument:
260
400
 
261
- #### Transport Support
401
+ ```ruby
402
+ configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
403
+ MCP::Server.new(name: "test_server", configuration: configuration)
404
+ ```
262
405
 
263
- - **stdio**: Notifications are sent as JSON-RPC 2.0 messages to stdout
264
- - **Streamable HTTP**: Notifications are sent as JSON-RPC 2.0 messages over HTTP with streaming (chunked transfer or SSE)
406
+ If no protocol version is specified, the latest stable version will be applied by default.
407
+ The latest stable version includes new features from the [draft version](https://modelcontextprotocol.io/specification/draft).
265
408
 
266
- #### Usage Example
409
+ 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`:
267
410
 
268
411
  ```ruby
269
- server = MCP::Server.new(name: "my_server")
412
+ MCP::Configuration.new(protocol_version: nil)
413
+ ```
270
414
 
271
- # Default Streamable HTTP - session oriented
272
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
415
+ If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
273
416
 
274
- server.transport = transport
417
+ 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.
275
418
 
276
- # When tools change, notify clients
277
- server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
278
- server.notify_tools_list_changed
419
+ ### Exception Reporting
420
+
421
+ The exception reporter receives two arguments:
422
+
423
+ - `exception`: The Ruby exception object that was raised
424
+ - `server_context`: A hash containing contextual information about where the error occurred
425
+
426
+ The server_context hash includes:
427
+
428
+ - For tool calls: `{ tool_name: "name", arguments: { ... } }`
429
+ - For general request handling: `{ request: { ... } }`
430
+
431
+ When an exception occurs:
432
+
433
+ 1. The exception is reported via the configured reporter
434
+ 2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
435
+ 3. For other requests, the exception is re-raised after reporting
436
+
437
+ If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
438
+
439
+ ### Tools
440
+
441
+ MCP spec includes [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) which provide functionality to LLM apps.
442
+
443
+ This gem provides a `MCP::Tool` class that can be used to create tools in three ways:
444
+
445
+ 1. As a class definition:
446
+
447
+ ```ruby
448
+ class MyTool < MCP::Tool
449
+ title "My Tool"
450
+ description "This tool performs specific functionality..."
451
+ input_schema(
452
+ properties: {
453
+ message: { type: "string" },
454
+ },
455
+ required: ["message"]
456
+ )
457
+ output_schema(
458
+ properties: {
459
+ result: { type: "string" },
460
+ success: { type: "boolean" },
461
+ timestamp: { type: "string", format: "date-time" }
462
+ },
463
+ required: ["result", "success", "timestamp"]
464
+ )
465
+ annotations(
466
+ read_only_hint: true,
467
+ destructive_hint: false,
468
+ idempotent_hint: true,
469
+ open_world_hint: false,
470
+ title: "My Tool"
471
+ )
472
+
473
+ def self.call(message:, server_context:)
474
+ MCP::Tool::Response.new([{ type: "text", text: "OK" }])
475
+ end
476
+ end
477
+
478
+ tool = MyTool
279
479
  ```
280
480
 
281
- You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
282
- This mode allows for easy multi-node deployment.
283
- Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new` (`stateless` defaults to `false`):
481
+ 2. By using the `MCP::Tool.define` method with a block:
284
482
 
285
483
  ```ruby
286
- # Stateless Streamable HTTP - session-less
287
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
484
+ tool = MCP::Tool.define(
485
+ name: "my_tool",
486
+ title: "My Tool",
487
+ description: "This tool performs specific functionality...",
488
+ annotations: {
489
+ read_only_hint: true,
490
+ title: "My Tool"
491
+ }
492
+ ) do |args, server_context:|
493
+ MCP::Tool::Response.new([{ type: "text", text: "OK" }])
494
+ end
288
495
  ```
289
496
 
290
- By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
291
- When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
497
+ 3. By using the `MCP::Server#define_tool` method with a block:
292
498
 
293
499
  ```ruby
294
- # Session timeout of 30 minutes
295
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
500
+ server = MCP::Server.new
501
+ server.define_tool(
502
+ name: "my_tool",
503
+ description: "This tool performs specific functionality...",
504
+ annotations: {
505
+ title: "My Tool",
506
+ read_only_hint: true
507
+ }
508
+ ) do |args, server_context:|
509
+ Tool::Response.new([{ type: "text", text: "OK" }])
510
+ end
296
511
  ```
297
512
 
298
- ### Unsupported Features (to be implemented in future versions)
513
+ The server_context parameter is the server_context passed into the server and can be used to pass per request information,
514
+ e.g. around authentication state.
515
+
516
+ ### Tool Annotations
299
517
 
300
- - Resource subscriptions
301
- - Completions
302
- - Elicitation
518
+ Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
303
519
 
304
- ### Usage
520
+ - `destructive_hint`: Indicates if the tool performs destructive operations. Defaults to true
521
+ - `idempotent_hint`: Indicates if the tool's operations are idempotent. Defaults to false
522
+ - `open_world_hint`: Indicates if the tool operates in an open world context. Defaults to true
523
+ - `read_only_hint`: Indicates if the tool only reads data (doesn't modify state). Defaults to false
524
+ - `title`: A human-readable title for the tool
525
+
526
+ Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
305
527
 
306
- #### Rails Controller
528
+ > [!NOTE]
529
+ > This **Tool Annotations** feature is supported starting from `protocol_version: '2025-03-26'`.
530
+
531
+ ### Tool Output Schemas
307
532
 
308
- When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streaming
309
- [Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http) transport
310
- requests.
533
+ 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:
311
534
 
312
- You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
313
- status codes (e.g., 202 Accepted for notifications).
535
+ 1. **Class definition with output_schema:**
314
536
 
315
537
  ```ruby
316
- class McpController < ActionController::Base
317
- def create
318
- server = MCP::Server.new(
319
- name: "my_server",
320
- title: "Example Server Display Name",
321
- version: "1.0.0",
322
- instructions: "Use the tools of this server as a last resort",
323
- tools: [SomeTool, AnotherTool],
324
- prompts: [MyPrompt],
325
- server_context: { user_id: current_user.id },
326
- )
327
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
328
- server.transport = transport
329
- status, headers, body = transport.handle_request(request)
538
+ class WeatherTool < MCP::Tool
539
+ tool_name "get_weather"
540
+ description "Get current weather for a location"
330
541
 
331
- render(json: body.first, status: status, headers: headers)
542
+ input_schema(
543
+ properties: {
544
+ location: { type: "string" },
545
+ units: { type: "string", enum: ["celsius", "fahrenheit"] }
546
+ },
547
+ required: ["location"]
548
+ )
549
+
550
+ output_schema(
551
+ properties: {
552
+ temperature: { type: "number" },
553
+ condition: { type: "string" },
554
+ humidity: { type: "integer" }
555
+ },
556
+ required: ["temperature", "condition", "humidity"]
557
+ )
558
+
559
+ def self.call(location:, units: "celsius", server_context:)
560
+ # Call weather API and structure the response
561
+ api_response = WeatherAPI.fetch(location, units)
562
+ weather_data = {
563
+ temperature: api_response.temp,
564
+ condition: api_response.description,
565
+ humidity: api_response.humidity_percent
566
+ }
567
+
568
+ output_schema.validate_result(weather_data)
569
+
570
+ MCP::Tool::Response.new([{
571
+ type: "text",
572
+ text: weather_data.to_json
573
+ }])
332
574
  end
333
575
  end
334
576
  ```
335
577
 
336
- #### Stdio Transport
578
+ 2. **Using Tool.define with output_schema:**
579
+
580
+ ```ruby
581
+ tool = MCP::Tool.define(
582
+ name: "calculate_stats",
583
+ description: "Calculate statistics for a dataset",
584
+ input_schema: {
585
+ properties: {
586
+ numbers: { type: "array", items: { type: "number" } }
587
+ },
588
+ required: ["numbers"]
589
+ },
590
+ output_schema: {
591
+ properties: {
592
+ mean: { type: "number" },
593
+ median: { type: "number" },
594
+ count: { type: "integer" }
595
+ },
596
+ required: ["mean", "median", "count"]
597
+ }
598
+ ) do |args, server_context:|
599
+ # Calculate statistics and validate against schema
600
+ MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
601
+ end
602
+ ```
603
+
604
+ 3. **Using OutputSchema objects:**
605
+
606
+ ```ruby
607
+ class DataTool < MCP::Tool
608
+ output_schema MCP::Tool::OutputSchema.new(
609
+ properties: {
610
+ success: { type: "boolean" },
611
+ data: { type: "object" }
612
+ },
613
+ required: ["success"]
614
+ )
615
+ end
616
+ ```
617
+
618
+ Output schema may also describe an array of objects:
619
+
620
+ ```ruby
621
+ class WeatherTool < MCP::Tool
622
+ output_schema(
623
+ type: "array",
624
+ items: {
625
+ properties: {
626
+ temperature: { type: "number" },
627
+ condition: { type: "string" },
628
+ humidity: { type: "integer" }
629
+ },
630
+ required: ["temperature", "condition", "humidity"]
631
+ }
632
+ )
633
+ end
634
+ ```
635
+
636
+ Please note: in this case, you must provide `type: "array"`. The default type
637
+ for output schemas is `object`.
638
+
639
+ MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/latest/server/tools#output-schema) specifies that:
640
+
641
+ - **Server Validation**: Servers MUST provide structured results that conform to the output schema
642
+ - **Client Validation**: Clients SHOULD validate structured results against the output schema
643
+ - **Better Integration**: Enables strict schema validation, type information, and improved developer experience
644
+ - **Backward Compatibility**: Tools returning structured content SHOULD also include serialized JSON in a TextContent block
645
+
646
+ The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
647
+
648
+ ### Tool Responses with Structured Content
649
+
650
+ Tools can return structured data alongside text content using the `structured_content` parameter.
651
+
652
+ The structured content will be included in the JSON-RPC response as the `structuredContent` field.
653
+
654
+ ```ruby
655
+ class WeatherTool < MCP::Tool
656
+ description "Get current weather and return structured data"
657
+
658
+ def self.call(location:, units: "celsius", server_context:)
659
+ # Call weather API and structure the response
660
+ api_response = WeatherAPI.fetch(location, units)
661
+ weather_data = {
662
+ temperature: api_response.temp,
663
+ condition: api_response.description,
664
+ humidity: api_response.humidity_percent
665
+ }
666
+
667
+ output_schema.validate_result(weather_data)
668
+
669
+ MCP::Tool::Response.new(
670
+ [{
671
+ type: "text",
672
+ text: weather_data.to_json
673
+ }],
674
+ structured_content: weather_data
675
+ )
676
+ end
677
+ end
678
+ ```
679
+
680
+ ### Tool Responses with Errors
681
+
682
+ Tools can return error information alongside text content using the `error` parameter.
683
+
684
+ The error will be included in the JSON-RPC response as the `isError` field.
685
+
686
+ ```ruby
687
+ class WeatherTool < MCP::Tool
688
+ description "Get current weather and return structured data"
689
+
690
+ def self.call(server_context:)
691
+ # Do something here
692
+ content = {}
693
+
694
+ MCP::Tool::Response.new(
695
+ [{
696
+ type: "text",
697
+ text: content.to_json
698
+ }],
699
+ structured_content: content,
700
+ error: true
701
+ )
702
+ end
703
+ end
704
+ ```
705
+
706
+ ### Prompts
707
+
708
+ 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.
709
+
710
+ The `MCP::Prompt` class provides three ways to create prompts:
711
+
712
+ 1. As a class definition with metadata:
713
+
714
+ ```ruby
715
+ class MyPrompt < MCP::Prompt
716
+ prompt_name "my_prompt" # Optional - defaults to underscored class name
717
+ title "My Prompt"
718
+ description "This prompt performs specific functionality..."
719
+ arguments [
720
+ MCP::Prompt::Argument.new(
721
+ name: "message",
722
+ title: "Message Title",
723
+ description: "Input message",
724
+ required: true
725
+ )
726
+ ]
727
+ meta({ version: "1.0", category: "example" })
728
+
729
+ class << self
730
+ def template(args, server_context:)
731
+ MCP::Prompt::Result.new(
732
+ description: "Response description",
733
+ messages: [
734
+ MCP::Prompt::Message.new(
735
+ role: "user",
736
+ content: MCP::Content::Text.new("User message")
737
+ ),
738
+ MCP::Prompt::Message.new(
739
+ role: "assistant",
740
+ content: MCP::Content::Text.new(args["message"])
741
+ )
742
+ ]
743
+ )
744
+ end
745
+ end
746
+ end
747
+
748
+ prompt = MyPrompt
749
+ ```
750
+
751
+ 2. Using the `MCP::Prompt.define` method:
752
+
753
+ ```ruby
754
+ prompt = MCP::Prompt.define(
755
+ name: "my_prompt",
756
+ title: "My Prompt",
757
+ description: "This prompt performs specific functionality...",
758
+ arguments: [
759
+ MCP::Prompt::Argument.new(
760
+ name: "message",
761
+ title: "Message Title",
762
+ description: "Input message",
763
+ required: true
764
+ )
765
+ ],
766
+ meta: { version: "1.0", category: "example" }
767
+ ) do |args, server_context:|
768
+ MCP::Prompt::Result.new(
769
+ description: "Response description",
770
+ messages: [
771
+ MCP::Prompt::Message.new(
772
+ role: "user",
773
+ content: MCP::Content::Text.new("User message")
774
+ ),
775
+ MCP::Prompt::Message.new(
776
+ role: "assistant",
777
+ content: MCP::Content::Text.new(args["message"])
778
+ )
779
+ ]
780
+ )
781
+ end
782
+ ```
783
+
784
+ 3. Using the `MCP::Server#define_prompt` method:
785
+
786
+ ```ruby
787
+ server = MCP::Server.new
788
+ server.define_prompt(
789
+ name: "my_prompt",
790
+ description: "This prompt performs specific functionality...",
791
+ arguments: [
792
+ Prompt::Argument.new(
793
+ name: "message",
794
+ title: "Message Title",
795
+ description: "Input message",
796
+ required: true
797
+ )
798
+ ],
799
+ meta: { version: "1.0", category: "example" }
800
+ ) do |args, server_context:|
801
+ Prompt::Result.new(
802
+ description: "Response description",
803
+ messages: [
804
+ Prompt::Message.new(
805
+ role: "user",
806
+ content: Content::Text.new("User message")
807
+ ),
808
+ Prompt::Message.new(
809
+ role: "assistant",
810
+ content: Content::Text.new(args["message"])
811
+ )
812
+ ]
813
+ )
814
+ end
815
+ ```
816
+
817
+ The server_context parameter is the server_context passed into the server and can be used to pass per request information,
818
+ e.g. around authentication state or user preferences.
819
+
820
+ ### Key Components
821
+
822
+ - `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
823
+ - `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
824
+ - `MCP::Prompt::Result` - The output of a prompt template containing description and messages
825
+ - `MCP::Content::Text` - Text content for messages
826
+
827
+ ### Usage
828
+
829
+ Register prompts with the MCP server:
830
+
831
+ ```ruby
832
+ server = MCP::Server.new(
833
+ name: "my_server",
834
+ prompts: [MyPrompt],
835
+ server_context: { user_id: current_user.id },
836
+ )
837
+ ```
838
+
839
+ The server will handle prompt listing and execution through the MCP protocol methods:
840
+
841
+ - `prompts/list` - Lists all registered prompts and their schemas
842
+ - `prompts/get` - Retrieves and executes a specific prompt with arguments
843
+
844
+ ### Resources
845
+
846
+ MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
847
+
848
+ ### Reading Resources
849
+
850
+ The `MCP::Resource` class provides a way to register resources with the server.
851
+
852
+ ```ruby
853
+ resource = MCP::Resource.new(
854
+ uri: "https://example.com/my_resource",
855
+ name: "my-resource",
856
+ title: "My Resource",
857
+ description: "Lorem ipsum dolor sit amet",
858
+ mime_type: "text/html",
859
+ )
860
+
861
+ server = MCP::Server.new(
862
+ name: "my_server",
863
+ resources: [resource],
864
+ )
865
+ ```
866
+
867
+ The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
868
+
869
+ ```ruby
870
+ server.resources_read_handler do |params|
871
+ [{
872
+ uri: params[:uri],
873
+ mimeType: "text/plain",
874
+ text: "Hello from example resource! URI: #{params[:uri]}"
875
+ }]
876
+ end
877
+ ```
878
+
879
+ otherwise `resources/read` requests will be a no-op.
880
+
881
+ ### Resource Templates
882
+
883
+ The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
884
+
885
+ ```ruby
886
+ resource_template = MCP::ResourceTemplate.new(
887
+ uri_template: "https://example.com/my_resource_template",
888
+ name: "my-resource-template",
889
+ title: "My Resource Template",
890
+ description: "Lorem ipsum dolor sit amet",
891
+ mime_type: "text/html",
892
+ )
893
+
894
+ server = MCP::Server.new(
895
+ name: "my_server",
896
+ resource_templates: [resource_template],
897
+ )
898
+ ```
899
+
900
+ ### Roots
901
+
902
+ The Model Context Protocol allows servers to request filesystem roots from clients through the `roots/list` method.
903
+ Roots define the boundaries of where a server can operate, providing a list of directories and files the client has made available.
904
+
905
+ **Key Concepts:**
337
906
 
338
- If you want to build a local command-line application, you can use the stdio transport:
907
+ - **Server-to-Client Request**: Like sampling, roots listing is initiated by the server
908
+ - **Client Capability**: Clients must declare `roots` capability during initialization
909
+ - **Change Notifications**: Clients that support `roots.listChanged` send `notifications/roots/list_changed` when roots change
339
910
 
340
- ```ruby
341
- require "mcp"
911
+ **Using Roots in Tools:**
342
912
 
343
- # Create a simple tool
344
- class ExampleTool < MCP::Tool
345
- description "A simple example tool that echoes back its arguments"
913
+ Tools that accept a `server_context:` parameter can call `list_roots` on it.
914
+ The request is automatically routed to the correct client session:
915
+
916
+ ```ruby
917
+ class FileSearchTool < MCP::Tool
918
+ description "Search files within the client's project roots"
346
919
  input_schema(
347
920
  properties: {
348
- message: { type: "string" },
921
+ query: { type: "string" }
349
922
  },
350
- required: ["message"]
923
+ required: ["query"]
351
924
  )
352
925
 
353
- class << self
354
- def call(message:, server_context:)
355
- MCP::Tool::Response.new([{
356
- type: "text",
357
- text: "Hello from example tool! Message: #{message}",
358
- }])
359
- end
926
+ def self.call(query:, server_context:)
927
+ roots = server_context.list_roots
928
+ root_uris = roots[:roots].map { |root| root[:uri] }
929
+
930
+ MCP::Tool::Response.new([{
931
+ type: "text",
932
+ text: "Searching in roots: #{root_uris.join(", ")}"
933
+ }])
360
934
  end
361
935
  end
362
-
363
- # Set up the server
364
- server = MCP::Server.new(
365
- name: "example_server",
366
- tools: [ExampleTool],
367
- )
368
-
369
- # Create and start the transport
370
- transport = MCP::Server::Transports::StdioTransport.new(server)
371
- transport.open
372
936
  ```
373
937
 
374
- You can run this script and then type in requests to the server at the command line.
938
+ Result contains an array of root objects:
375
939
 
376
- ```console
377
- $ ruby examples/stdio_server.rb
378
- {"jsonrpc":"2.0","id":"1","method":"ping"}
379
- {"jsonrpc":"2.0","id":"2","method":"tools/list"}
380
- {"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
940
+ ```ruby
941
+ {
942
+ roots: [
943
+ { uri: "file:///home/user/projects/myproject", name: "My Project" },
944
+ { uri: "file:///home/user/repos/backend", name: "Backend Repository" }
945
+ ]
946
+ }
381
947
  ```
382
948
 
383
- ### Configuration
949
+ **Handling Root Changes:**
384
950
 
385
- The gem can be configured using the `MCP.configure` block:
951
+ Register a callback to be notified when the client's roots change:
386
952
 
387
953
  ```ruby
388
- MCP.configure do |config|
389
- config.exception_reporter = ->(exception, server_context) {
390
- # Your exception reporting logic here
391
- # For example with Bugsnag:
392
- Bugsnag.notify(exception) do |report|
393
- report.add_metadata(:model_context_protocol, server_context)
394
- end
395
- }
396
-
397
- config.instrumentation_callback = ->(data) {
398
- puts "Got instrumentation data #{data.inspect}"
399
- }
954
+ server.roots_list_changed_handler do
955
+ puts "Client's roots have changed, tools will see updated roots on next call."
400
956
  end
401
957
  ```
402
958
 
403
- or by creating an explicit configuration and passing it into the server.
404
- This is useful for systems where an application hosts more than one MCP server but
405
- they might require different instrumentation callbacks.
406
-
407
- ```ruby
408
- configuration = MCP::Configuration.new
409
- configuration.exception_reporter = ->(exception, server_context) {
410
- # Your exception reporting logic here
411
- # For example with Bugsnag:
412
- Bugsnag.notify(exception) do |report|
413
- report.add_metadata(:model_context_protocol, server_context)
414
- end
415
- }
416
-
417
- configuration.instrumentation_callback = ->(data) {
418
- puts "Got instrumentation data #{data.inspect}"
419
- }
959
+ **Error Handling:**
420
960
 
421
- server = MCP::Server.new(
422
- # ... all other options
423
- configuration:,
424
- )
425
- ```
961
+ - Raises `RuntimeError` if client does not support `roots` capability
962
+ - Raises `StandardError` if client returns an error response
426
963
 
427
- ### Server Context and Configuration Block Data
964
+ ### Resource Subscriptions
428
965
 
429
- #### `server_context`
966
+ Resource subscriptions allow clients to monitor specific resources for changes.
967
+ When a subscribed resource is updated, the server sends a notification to the client.
430
968
 
431
- The `server_context` is a user-defined hash that is passed into the server instance and made available to tools, prompts, and exception/instrumentation callbacks. It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
969
+ The SDK does not track subscription state internally.
970
+ Server developers register handlers and manage their own subscription state.
971
+ Three methods are provided:
432
972
 
433
- **Type:**
973
+ - `Server#resources_subscribe_handler` - registers a handler for `resources/subscribe` requests
974
+ - `Server#resources_unsubscribe_handler` - registers a handler for `resources/unsubscribe` requests
975
+ - `ServerContext#notify_resources_updated` - sends a `notifications/resources/updated` notification to the subscribing client
434
976
 
435
977
  ```ruby
436
- server_context: { [String, Symbol] => Any }
437
- ```
438
-
439
- **Example:**
978
+ subscribed_uris = Set.new
440
979
 
441
- ```ruby
442
980
  server = MCP::Server.new(
443
981
  name: "my_server",
444
- server_context: { user_id: current_user.id, request_id: request.uuid }
982
+ resources: [my_resource],
983
+ capabilities: { resources: { subscribe: true } },
445
984
  )
985
+
986
+ server.resources_subscribe_handler do |params|
987
+ subscribed_uris.add(params[:uri].to_s)
988
+ end
989
+
990
+ server.resources_unsubscribe_handler do |params|
991
+ subscribed_uris.delete(params[:uri].to_s)
992
+ end
993
+
994
+ server.define_tool(name: "update_resource") do |server_context:, **args|
995
+ if subscribed_uris.include?("test://my-resource")
996
+ server_context.notify_resources_updated(uri: "test://my-resource")
997
+ end
998
+ MCP::Tool::Response.new([MCP::Content::Text.new("Resource updated").to_h])
999
+ end
446
1000
  ```
447
1001
 
448
- This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
1002
+ ### Sampling
449
1003
 
450
- #### Request-specific `_meta` Parameter
1004
+ The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.
1005
+ This enables servers to leverage the client's LLM capabilities without needing direct access to AI models.
451
1006
 
452
- 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`.
1007
+ **Key Concepts:**
453
1008
 
454
- **Access Pattern:**
1009
+ - **Server-to-Client Request**: Unlike typical MCP methods (client to server), sampling is initiated by the server
1010
+ - **Client Capability**: Clients must declare `sampling` capability during initialization
1011
+ - **Tool Support**: When using tools in sampling requests, clients must declare `sampling.tools` capability
1012
+ - **Human-in-the-Loop**: Clients can implement user approval before forwarding requests to LLMs
455
1013
 
456
- When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
1014
+ **Using Sampling in Tools:**
1015
+
1016
+ Tools that accept a `server_context:` parameter can call `create_sampling_message` on it.
1017
+ The request is automatically routed to the correct client session:
457
1018
 
458
1019
  ```ruby
459
- class MyTool < MCP::Tool
460
- def self.call(message:, server_context:)
461
- # Access provider-specific metadata
462
- session_id = server_context.dig(:_meta, :session_id)
463
- request_id = server_context.dig(:_meta, :request_id)
1020
+ class SummarizeTool < MCP::Tool
1021
+ description "Summarize text using LLM"
1022
+ input_schema(
1023
+ properties: {
1024
+ text: { type: "string" }
1025
+ },
1026
+ required: ["text"]
1027
+ )
464
1028
 
465
- # Access server's original context
466
- user_id = server_context.dig(:user_id)
1029
+ def self.call(text:, server_context:)
1030
+ result = server_context.create_sampling_message(
1031
+ messages: [
1032
+ { role: "user", content: { type: "text", text: "Please summarize: #{text}" } }
1033
+ ],
1034
+ max_tokens: 500
1035
+ )
467
1036
 
468
1037
  MCP::Tool::Response.new([{
469
1038
  type: "text",
470
- text: "Processing for user #{user_id} in session #{session_id}"
1039
+ text: result[:content][:text]
471
1040
  }])
472
1041
  end
473
1042
  end
1043
+
1044
+ server = MCP::Server.new(name: "my_server", tools: [SummarizeTool])
474
1045
  ```
475
1046
 
476
- **Client Request Example:**
1047
+ **Parameters:**
477
1048
 
478
- ```json
479
- {
480
- "jsonrpc": "2.0",
481
- "id": 1,
482
- "method": "tools/call",
483
- "params": {
484
- "name": "my_tool",
485
- "arguments": { "message": "Hello" },
486
- "_meta": {
487
- "session_id": "abc123",
488
- "request_id": "req_456"
489
- }
490
- }
491
- }
492
- ```
1049
+ Required:
493
1050
 
494
- #### Configuration Block Data
1051
+ - `messages:` (Array) - Array of message objects with `role` and `content`
1052
+ - `max_tokens:` (Integer) - Maximum tokens in the response
495
1053
 
496
- ##### Exception Reporter
1054
+ Optional:
497
1055
 
498
- The exception reporter receives:
1056
+ - `system_prompt:` (String) - System prompt for the LLM
1057
+ - `model_preferences:` (Hash) - Model selection preferences (e.g., `{ intelligencePriority: 0.8 }`)
1058
+ - `include_context:` (String) - Context inclusion: `"none"`, `"thisServer"`, or `"allServers"` (soft-deprecated)
1059
+ - `temperature:` (Float) - Sampling temperature
1060
+ - `stop_sequences:` (Array) - Sequences that stop generation
1061
+ - `metadata:` (Hash) - Additional metadata
1062
+ - `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
1063
+ - `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)
499
1064
 
500
- - `exception`: The Ruby exception object that was raised
501
- - `server_context`: The context hash provided to the server
1065
+ **Error Handling:**
502
1066
 
503
- **Signature:**
1067
+ - Raises `RuntimeError` if client does not support `sampling` capability
1068
+ - Raises `RuntimeError` if `tools` are used but client lacks `sampling.tools` capability
1069
+ - Raises `StandardError` if client returns an error response
504
1070
 
505
- ```ruby
506
- exception_reporter = ->(exception, server_context) { ... }
507
- ```
1071
+ ### Notifications
508
1072
 
509
- ##### Instrumentation Callback
1073
+ The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
510
1074
 
511
- The instrumentation callback receives a hash with the following possible keys:
1075
+ #### Notification Methods
512
1076
 
513
- - `method`: (String) The protocol method called (e.g., "ping", "tools/list")
514
- - `tool_name`: (String, optional) The name of the tool called
515
- - `tool_arguments`: (Hash, optional) The arguments passed to the tool
516
- - `prompt_name`: (String, optional) The name of the prompt called
517
- - `resource_uri`: (String, optional) The URI of the resource called
518
- - `error`: (String, optional) Error code if a lookup failed
519
- - `duration`: (Float) Duration of the call in seconds
520
- - `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
1077
+ The server provides the following notification methods:
1078
+
1079
+ - `notify_tools_list_changed` - Send a notification when the tools list changes
1080
+ - `notify_prompts_list_changed` - Send a notification when the prompts list changes
1081
+ - `notify_resources_list_changed` - Send a notification when the resources list changes
1082
+ - `notify_log_message` - Send a structured logging notification message
1083
+
1084
+ #### Session Scoping
1085
+
1086
+ When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
1087
+
1088
+ - **`report_progress`** and **`notify_log_message`** called via `server_context` inside a tool handler are automatically sent only to the requesting client.
1089
+ No extra configuration is needed.
1090
+ - **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
1091
+ as they represent server-wide state changes. These should be called on the `server` instance directly.
1092
+
1093
+ #### Notification Format
1094
+
1095
+ Notifications follow the JSON-RPC 2.0 specification and use these method names:
1096
+
1097
+ - `notifications/tools/list_changed`
1098
+ - `notifications/prompts/list_changed`
1099
+ - `notifications/resources/list_changed`
1100
+ - `notifications/cancelled`
1101
+ - `notifications/progress`
1102
+ - `notifications/message`
1103
+
1104
+ ### Cancellation
1105
+
1106
+ The MCP Ruby SDK supports server-side handling of the
1107
+ [MCP `notifications/cancelled` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation).
1108
+ When a client sends `notifications/cancelled` for an in-flight request, the server stops
1109
+ processing cooperatively and suppresses the JSON-RPC response for that request.
1110
+
1111
+ Cancellation is cooperative: the SDK does not forcibly terminate tool code. Instead,
1112
+ a `MCP::Cancellation` token is threaded through `server_context`, and long-running tools
1113
+ poll it to exit early. When a tool returns after cancellation has been observed,
1114
+ the server suppresses the JSON-RPC response, matching the spec. The `initialize` request
1115
+ is never cancellable per the spec.
521
1116
 
522
1117
  > [!NOTE]
523
- > `tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
524
- > This is to avoid potential issues with metric cardinality.
1118
+ > Client-initiated cancellation (`Client#cancel` equivalent that would also abort
1119
+ > the calling thread's wait) is not yet implemented. Sending `notifications/cancelled`
1120
+ > from the client side can be done by constructing the notification payload and writing it
1121
+ > directly through the transport, but the calling thread does not yet unwind automatically.
1122
+ > This is tracked as a follow-up.
525
1123
 
526
- **Type:**
1124
+ #### Server-Side: Handlers that Check for Cancellation
1125
+
1126
+ Any handler that opts in to `server_context:` - tools (`Tool.call`), prompt templates,
1127
+ `resources_read_handler`, `completion_handler`, `resources_subscribe_handler`,
1128
+ `resources_unsubscribe_handler`, and `define_custom_method` blocks - receives
1129
+ an `MCP::ServerContext` wired to the in-flight request's cancellation token.
1130
+ Handlers check `cancelled?` in their work loop, or call `raise_if_cancelled!` to raise
1131
+ `MCP::CancelledError` at a safe point:
527
1132
 
528
1133
  ```ruby
529
- instrumentation_callback = ->(data) { ... }
530
- # where data is a Hash with keys as described above
1134
+ class LongRunningTool < MCP::Tool
1135
+ description "A tool that supports cancellation"
1136
+ input_schema(properties: { count: { type: "integer" } }, required: ["count"])
1137
+
1138
+ def self.call(count:, server_context:)
1139
+ count.times do |i|
1140
+ # Exit early if the client has sent `notifications/cancelled`.
1141
+ break if server_context.cancelled?
1142
+
1143
+ do_work(i)
1144
+ end
1145
+
1146
+ MCP::Tool::Response.new([{ type: "text", text: "Done" }])
1147
+ end
1148
+ end
531
1149
  ```
532
1150
 
533
- **Example:**
1151
+ Alternatively, raise at the next safe point with `raise_if_cancelled!`:
534
1152
 
535
1153
  ```ruby
536
- MCP.configure do |config|
537
- config.instrumentation_callback = ->(data) {
538
- puts "Instrumentation: #{data.inspect}"
539
- }
1154
+ def self.call(count:, server_context:)
1155
+ count.times do |i|
1156
+ server_context.raise_if_cancelled!
1157
+
1158
+ do_work(i)
1159
+ end
1160
+
1161
+ MCP::Tool::Response.new([{ type: "text", text: "Done" }])
540
1162
  end
541
1163
  ```
542
1164
 
543
- ### Server Protocol Version
1165
+ When a handler observes cancellation (either by returning early with `cancelled?` or
1166
+ by raising `MCP::CancelledError` via `raise_if_cancelled!`), the server drops the response and
1167
+ no JSON-RPC result is sent to the client.
544
1168
 
545
- The server's protocol version can be overridden using the `protocol_version` keyword argument:
1169
+ The same pattern works for other handler types:
546
1170
 
547
1171
  ```ruby
548
- configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
549
- MCP::Server.new(name: "test_server", configuration: configuration)
1172
+ # resources/read
1173
+ server.resources_read_handler do |params, server_context:|
1174
+ server_context.raise_if_cancelled!
1175
+ # read the resource
1176
+ end
1177
+
1178
+ # completion/complete
1179
+ server.completion_handler do |params, server_context:|
1180
+ server_context.raise_if_cancelled!
1181
+ # compute completions
1182
+ end
1183
+
1184
+ # custom method
1185
+ server.define_custom_method(method_name: "custom/slow") do |params, server_context:|
1186
+ server_context.raise_if_cancelled!
1187
+ # do work
1188
+ end
1189
+
1190
+ # prompts (via Prompt subclass)
1191
+ class SlowPrompt < MCP::Prompt
1192
+ prompt_name "slow_prompt"
1193
+
1194
+ def self.template(args, server_context:)
1195
+ server_context.raise_if_cancelled!
1196
+ MCP::Prompt::Result.new(messages: [])
1197
+ end
1198
+ end
550
1199
  ```
551
1200
 
552
- If no protocol version is specified, the latest stable version will be applied by default.
553
- The latest stable version includes new features from the [draft version](https://modelcontextprotocol.io/specification/draft).
1201
+ Handlers that do not declare a `server_context:` keyword continue to work unchanged -
1202
+ the opt-in detection only wraps the context when the block signature asks for it.
554
1203
 
555
- 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`:
1204
+ #### Nested Server-to-Client Requests Are Cancelled Automatically
1205
+
1206
+ When a tool handler is waiting on a nested server-to-client request
1207
+ (`server_context.create_sampling_message`, `create_form_elicitation`, or
1208
+ `create_url_elicitation`), cancelling the parent tool call automatically raises
1209
+ `MCP::CancelledError` from the nested call, so the tool does not need to wrap it
1210
+ in its own `cancelled?` checks:
556
1211
 
557
1212
  ```ruby
558
- MCP::Configuration.new(protocol_version: nil)
1213
+ def self.call(server_context:)
1214
+ result = server_context.create_sampling_message(messages: messages, max_tokens: 100)
1215
+ # If the parent tools/call is cancelled while waiting above, MCP::CancelledError
1216
+ # is raised here and the tool can let it propagate or clean up as needed.
1217
+ MCP::Tool::Response.new([{ type: "text", text: result[:content][:text] }])
1218
+ rescue MCP::CancelledError
1219
+ # Optional: run cleanup. Re-raising (or letting it propagate) is fine; the server
1220
+ # will still suppress the JSON-RPC response per the MCP spec.
1221
+ raise
1222
+ end
559
1223
  ```
560
1224
 
561
- If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
1225
+ Nested cancellation propagation is supported on `StreamableHTTPTransport` only.
1226
+ `StdioTransport` is single-threaded and blocks on `$stdin.gets`, so a nested
1227
+ `server_context.create_sampling_message` inside a tool runs to completion even if
1228
+ the parent `tools/call` is cancelled. The parent tool itself still observes cancellation
1229
+ via `server_context.cancelled?` between nested calls.
562
1230
 
563
- 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.
1231
+ ### Ping
564
1232
 
565
- ### Exception Reporting
1233
+ The MCP Ruby SDK supports the
1234
+ [MCP `ping` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping),
1235
+ which allows either side of the connection to verify that the peer is still responsive.
1236
+ A `ping` request has no parameters, and the receiver MUST respond promptly with an empty result.
566
1237
 
567
- The exception reporter receives two arguments:
1238
+ #### Server-Side
568
1239
 
569
- - `exception`: The Ruby exception object that was raised
570
- - `server_context`: A hash containing contextual information about where the error occurred
1240
+ Servers respond to incoming `ping` requests automatically - no setup is required.
1241
+ Any `MCP::Server` instance replies with an empty result.
571
1242
 
572
- The server_context hash includes:
1243
+ #### Client-Side
573
1244
 
574
- - For tool calls: `{ tool_name: "name", arguments: { ... } }`
575
- - For general request handling: `{ request: { ... } }`
1245
+ `MCP::Client` exposes `ping` to send a ping to the server:
576
1246
 
577
- When an exception occurs:
1247
+ ```ruby
1248
+ client = MCP::Client.new(transport: transport)
1249
+ client.ping # => {} on success
1250
+ ```
578
1251
 
579
- 1. The exception is reported via the configured reporter
580
- 2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
581
- 3. For other requests, the exception is re-raised after reporting
1252
+ `#ping` raises `MCP::Client::ServerError` when the server returns a JSON-RPC error.
1253
+ It raises `MCP::Client::ValidationError` when the response `result` is missing or
1254
+ is not a Hash (matching the spec requirement that `result` be an object).
1255
+ Transport-level errors (for example, `MCP::Client::Stdio`'s `read_timeout:` firing)
1256
+ propagate as exceptions raised by the transport layer.
582
1257
 
583
- If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
1258
+ ### Progress
584
1259
 
585
- ### Tools
1260
+ The MCP Ruby SDK supports progress tracking for long-running tool operations,
1261
+ following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
586
1262
 
587
- MCP spec includes [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) which provide functionality to LLM apps.
1263
+ #### How Progress Works
588
1264
 
589
- This gem provides a `MCP::Tool` class that can be used to create tools in three ways:
1265
+ 1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
1266
+ 2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
1267
+ 3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
590
1268
 
591
- 1. As a class definition:
1269
+ #### Server-Side: Tool with Progress
1270
+
1271
+ Tools that accept a `server_context:` parameter can call `report_progress` on it.
1272
+ The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
592
1273
 
593
1274
  ```ruby
594
- class MyTool < MCP::Tool
595
- title "My Tool"
596
- description "This tool performs specific functionality..."
1275
+ class LongRunningTool < MCP::Tool
1276
+ description "A tool that reports progress during execution"
597
1277
  input_schema(
598
1278
  properties: {
599
- message: { type: "string" },
600
- },
601
- required: ["message"]
602
- )
603
- output_schema(
604
- properties: {
605
- result: { type: "string" },
606
- success: { type: "boolean" },
607
- timestamp: { type: "string", format: "date-time" }
1279
+ count: { type: "integer" },
608
1280
  },
609
- required: ["result", "success", "timestamp"]
610
- )
611
- annotations(
612
- read_only_hint: true,
613
- destructive_hint: false,
614
- idempotent_hint: true,
615
- open_world_hint: false,
616
- title: "My Tool"
1281
+ required: ["count"]
617
1282
  )
618
1283
 
619
- def self.call(message:, server_context:)
620
- MCP::Tool::Response.new([{ type: "text", text: "OK" }])
1284
+ def self.call(count:, server_context:)
1285
+ count.times do |i|
1286
+ # Do work here.
1287
+ server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
1288
+ end
1289
+
1290
+ MCP::Tool::Response.new([{ type: "text", text: "Done" }])
621
1291
  end
622
1292
  end
623
-
624
- tool = MyTool
625
1293
  ```
626
1294
 
627
- 2. By using the `MCP::Tool.define` method with a block:
1295
+ The `server_context.report_progress` method accepts:
628
1296
 
629
- ```ruby
630
- tool = MCP::Tool.define(
631
- name: "my_tool",
632
- title: "My Tool",
633
- description: "This tool performs specific functionality...",
634
- annotations: {
635
- read_only_hint: true,
636
- title: "My Tool"
637
- }
638
- ) do |args, server_context:|
639
- MCP::Tool::Response.new([{ type: "text", text: "OK" }])
640
- end
641
- ```
1297
+ - `progress` (required) — current progress value (numeric)
1298
+ - `total:` (optional) — total expected value, so clients can display a percentage
1299
+ - `message:` (optional) — human-readable status message
642
1300
 
643
- 3. By using the `MCP::Server#define_tool` method with a block:
1301
+ **Key Features:**
1302
+
1303
+ - Tools report progress via `server_context.report_progress`
1304
+ - `report_progress` is a no-op when no `progressToken` was provided by the client
1305
+ - Supports both numeric and string progress tokens
1306
+
1307
+ ### Completions
1308
+
1309
+ MCP spec includes [Completions](https://modelcontextprotocol.io/specification/latest/server/utilities/completion),
1310
+ which enable servers to provide autocompletion suggestions for prompt arguments and resource URIs.
1311
+
1312
+ To enable completions, declare the `completions` capability and register a handler:
644
1313
 
645
1314
  ```ruby
646
- server = MCP::Server.new
647
- server.define_tool(
648
- name: "my_tool",
649
- description: "This tool performs specific functionality...",
650
- annotations: {
651
- title: "My Tool",
652
- read_only_hint: true
653
- }
654
- ) do |args, server_context:|
655
- Tool::Response.new([{ type: "text", text: "OK" }])
1315
+ server = MCP::Server.new(
1316
+ name: "my_server",
1317
+ prompts: [CodeReviewPrompt],
1318
+ resource_templates: [FileTemplate],
1319
+ capabilities: { completions: {} },
1320
+ )
1321
+
1322
+ server.completion_handler do |params|
1323
+ ref = params[:ref]
1324
+ argument = params[:argument]
1325
+ value = argument[:value]
1326
+
1327
+ case ref[:type]
1328
+ when "ref/prompt"
1329
+ values = case argument[:name]
1330
+ when "language"
1331
+ ["python", "pytorch", "pyside"].select { |v| v.start_with?(value) }
1332
+ else
1333
+ []
1334
+ end
1335
+ { completion: { values: values, hasMore: false } }
1336
+ when "ref/resource"
1337
+ { completion: { values: [], hasMore: false } }
1338
+ end
656
1339
  end
657
1340
  ```
658
1341
 
659
- The server_context parameter is the server_context passed into the server and can be used to pass per request information,
660
- e.g. around authentication state.
1342
+ The handler receives a `params` hash with:
661
1343
 
662
- ### Tool Annotations
1344
+ - `ref` - The reference (`{ type: "ref/prompt", name: "..." }` or `{ type: "ref/resource", uri: "..." }`)
1345
+ - `argument` - The argument being completed (`{ name: "...", value: "..." }`)
1346
+ - `context` (optional) - Previously resolved arguments (`{ arguments: { ... } }`)
663
1347
 
664
- Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
1348
+ The handler must return a hash with a `completion` key containing `values` (array of strings), and optionally `total` and `hasMore`.
1349
+ The SDK automatically enforces the 100-item limit per the MCP specification.
665
1350
 
666
- - `destructive_hint`: Indicates if the tool performs destructive operations. Defaults to true
667
- - `idempotent_hint`: Indicates if the tool's operations are idempotent. Defaults to false
668
- - `open_world_hint`: Indicates if the tool operates in an open world context. Defaults to true
669
- - `read_only_hint`: Indicates if the tool only reads data (doesn't modify state). Defaults to false
670
- - `title`: A human-readable title for the tool
1351
+ The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
1352
+ Requests for unknown references return an error.
671
1353
 
672
- Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
1354
+ ### Elicitation
673
1355
 
674
- > [!NOTE]
675
- > This **Tool Annotations** feature is supported starting from `protocol_version: '2025-03-26'`.
1356
+ The MCP Ruby SDK supports [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation),
1357
+ which allows servers to request additional information from users through the client during tool execution.
676
1358
 
677
- ### Tool Output Schemas
1359
+ Elicitation is a **server-to-client request**. The server sends a request and blocks until the user responds via the client.
678
1360
 
679
- 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:
1361
+ #### Capabilities
680
1362
 
681
- 1. **Class definition with output_schema:**
1363
+ Clients must declare the `elicitation` capability during initialization. The server checks this before sending any elicitation request
1364
+ and raises a `RuntimeError` if the client does not support it.
682
1365
 
683
- ```ruby
684
- class WeatherTool < MCP::Tool
685
- tool_name "get_weather"
686
- description "Get current weather for a location"
1366
+ For URL mode support, the client must also declare `elicitation.url` capability.
687
1367
 
688
- input_schema(
689
- properties: {
690
- location: { type: "string" },
691
- units: { type: "string", enum: ["celsius", "fahrenheit"] }
692
- },
693
- required: ["location"]
694
- )
1368
+ #### Using Elicitation in Tools
695
1369
 
696
- output_schema(
697
- properties: {
698
- temperature: { type: "number" },
699
- condition: { type: "string" },
700
- humidity: { type: "integer" }
1370
+ Tools that accept a `server_context:` parameter can call `create_form_elicitation` on it:
1371
+
1372
+ ```ruby
1373
+ server.define_tool(name: "collect_info", description: "Collect user info") do |server_context:|
1374
+ result = server_context.create_form_elicitation(
1375
+ message: "Please provide your name",
1376
+ requested_schema: {
1377
+ type: "object",
1378
+ properties: { name: { type: "string" } },
1379
+ required: ["name"],
701
1380
  },
702
- required: ["temperature", "condition", "humidity"]
703
1381
  )
704
1382
 
705
- def self.call(location:, units: "celsius", server_context:)
706
- # Call weather API and structure the response
707
- api_response = WeatherAPI.fetch(location, units)
708
- weather_data = {
709
- temperature: api_response.temp,
710
- condition: api_response.description,
711
- humidity: api_response.humidity_percent
712
- }
713
-
714
- output_schema.validate_result(weather_data)
715
-
716
- MCP::Tool::Response.new([{
717
- type: "text",
718
- text: weather_data.to_json
719
- }])
720
- end
1383
+ MCP::Tool::Response.new([{ type: "text", text: "Hello, #{result[:content][:name]}" }])
721
1384
  end
722
1385
  ```
723
1386
 
724
- 2. **Using Tool.define with output_schema:**
1387
+ #### Form Mode
1388
+
1389
+ Form mode collects structured data from the user directly through the MCP client:
725
1390
 
726
1391
  ```ruby
727
- tool = MCP::Tool.define(
728
- name: "calculate_stats",
729
- description: "Calculate statistics for a dataset",
730
- input_schema: {
731
- properties: {
732
- numbers: { type: "array", items: { type: "number" } }
733
- },
734
- required: ["numbers"]
735
- },
736
- output_schema: {
737
- properties: {
738
- mean: { type: "number" },
739
- median: { type: "number" },
740
- count: { type: "integer" }
1392
+ server.define_tool(name: "collect_contact", description: "Collect contact info") do |server_context:|
1393
+ result = server_context.create_form_elicitation(
1394
+ message: "Please provide your contact information",
1395
+ requested_schema: {
1396
+ type: "object",
1397
+ properties: {
1398
+ name: { type: "string", description: "Your full name" },
1399
+ email: { type: "string", format: "email", description: "Your email address" },
1400
+ },
1401
+ required: ["name", "email"],
741
1402
  },
742
- required: ["mean", "median", "count"]
743
- }
744
- ) do |args, server_context:|
745
- # Calculate statistics and validate against schema
746
- MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
1403
+ )
1404
+
1405
+ text = case result[:action]
1406
+ when "accept"
1407
+ "Hello, #{result[:content][:name]} (#{result[:content][:email]})"
1408
+ when "decline"
1409
+ "User declined"
1410
+ when "cancel"
1411
+ "User cancelled"
1412
+ end
1413
+
1414
+ MCP::Tool::Response.new([{ type: "text", text: text }])
747
1415
  end
748
1416
  ```
749
1417
 
750
- 3. **Using OutputSchema objects:**
1418
+ #### URL Mode
1419
+
1420
+ URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
751
1421
 
752
1422
  ```ruby
753
- class DataTool < MCP::Tool
754
- output_schema MCP::Tool::OutputSchema.new(
755
- properties: {
756
- success: { type: "boolean" },
757
- data: { type: "object" }
758
- },
759
- required: ["success"]
1423
+ server.define_tool(name: "authorize_github", description: "Authorize GitHub") do |server_context:|
1424
+ elicitation_id = SecureRandom.uuid
1425
+
1426
+ result = server_context.create_url_elicitation(
1427
+ message: "Please authorize access to your GitHub account",
1428
+ url: "https://example.com/oauth/authorize?elicitation_id=#{elicitation_id}",
1429
+ elicitation_id: elicitation_id,
760
1430
  )
1431
+
1432
+ server_context.notify_elicitation_complete(elicitation_id: elicitation_id)
1433
+
1434
+ MCP::Tool::Response.new([{ type: "text", text: "Authorization complete" }])
761
1435
  end
762
1436
  ```
763
1437
 
764
- Output schema may also describe an array of objects:
1438
+ #### URLElicitationRequiredError
1439
+
1440
+ When a tool cannot proceed until an out-of-band elicitation is completed, raise `MCP::Server::URLElicitationRequiredError`.
1441
+ This returns a JSON-RPC error with code `-32042` to the client:
765
1442
 
766
1443
  ```ruby
767
- class WeatherTool < MCP::Tool
768
- output_schema(
769
- type: "array",
770
- items: {
771
- properties: {
772
- temperature: { type: "number" },
773
- condition: { type: "string" },
774
- humidity: { type: "integer" }
775
- },
776
- required: ["temperature", "condition", "humidity"]
777
- }
778
- )
1444
+ server.define_tool(name: "access_github", description: "Access GitHub") do |server_context:|
1445
+ raise MCP::Server::URLElicitationRequiredError.new([
1446
+ {
1447
+ mode: "url",
1448
+ elicitationId: SecureRandom.uuid,
1449
+ url: "https://example.com/oauth/authorize",
1450
+ message: "GitHub authorization is required.",
1451
+ },
1452
+ ])
779
1453
  end
780
1454
  ```
781
1455
 
782
- Please note: in this case, you must provide `type: "array"`. The default type
783
- for output schemas is `object`.
1456
+ ### Logging
784
1457
 
785
- MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/latest/server/tools#output-schema) specifies that:
1458
+ 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).
786
1459
 
787
- - **Server Validation**: Servers MUST provide structured results that conform to the output schema
788
- - **Client Validation**: Clients SHOULD validate structured results against the output schema
789
- - **Better Integration**: Enables strict schema validation, type information, and improved developer experience
790
- - **Backward Compatibility**: Tools returning structured content SHOULD also include serialized JSON in a TextContent block
1460
+ The `notifications/message` notification is used for structured logging between client and server.
791
1461
 
792
- The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
1462
+ #### Log Levels
793
1463
 
794
- ### Tool Responses with Structured Content
1464
+ The SDK supports 8 log levels with increasing severity:
795
1465
 
796
- Tools can return structured data alongside text content using the `structured_content` parameter.
1466
+ - `debug` - Detailed debugging information
1467
+ - `info` - General informational messages
1468
+ - `notice` - Normal but significant events
1469
+ - `warning` - Warning conditions
1470
+ - `error` - Error conditions
1471
+ - `critical` - Critical conditions
1472
+ - `alert` - Action must be taken immediately
1473
+ - `emergency` - System is unusable
797
1474
 
798
- The structured content will be included in the JSON-RPC response as the `structuredContent` field.
1475
+ #### How Logging Works
1476
+
1477
+ 1. **Client Configuration**: The client sends a `logging/setLevel` request to configure the minimum log level
1478
+ 2. **Server Filtering**: The server only sends log messages at the configured level or higher severity
1479
+ 3. **Notification Delivery**: Log messages are sent as `notifications/message` to the client
1480
+
1481
+ For example, if the client sets the level to `"error"` (severity 4), the server will send messages with levels: `error`, `critical`, `alert`, and `emergency`.
1482
+
1483
+ For more details, see the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
1484
+
1485
+ **Usage Example:**
799
1486
 
800
1487
  ```ruby
801
- class WeatherTool < MCP::Tool
802
- description "Get current weather and return structured data"
1488
+ server = MCP::Server.new(name: "my_server")
1489
+ transport = MCP::Server::Transports::StdioTransport.new(server)
803
1490
 
804
- def self.call(location:, units: "celsius", server_context:)
805
- # Call weather API and structure the response
806
- api_response = WeatherAPI.fetch(location, units)
807
- weather_data = {
808
- temperature: api_response.temp,
809
- condition: api_response.description,
810
- humidity: api_response.humidity_percent
811
- }
1491
+ # The client first configures the logging level (on the client side):
1492
+ transport.send_request(
1493
+ request: {
1494
+ jsonrpc: "2.0",
1495
+ method: "logging/setLevel",
1496
+ params: { level: "info" },
1497
+ id: session_id # Unique request ID within the session
1498
+ }
1499
+ )
812
1500
 
813
- output_schema.validate_result(weather_data)
1501
+ # Send log messages at different severity levels
1502
+ server.notify_log_message(
1503
+ data: { message: "Application started successfully" },
1504
+ level: "info"
1505
+ )
814
1506
 
815
- MCP::Tool::Response.new(
816
- [{
817
- type: "text",
818
- text: weather_data.to_json
819
- }],
820
- structured_content: weather_data
821
- )
822
- end
823
- end
1507
+ server.notify_log_message(
1508
+ data: { message: "Configuration file not found, using defaults" },
1509
+ level: "warning"
1510
+ )
1511
+
1512
+ server.notify_log_message(
1513
+ data: {
1514
+ error: "Database connection failed",
1515
+ details: { host: "localhost", port: 5432 }
1516
+ },
1517
+ level: "error",
1518
+ logger: "DatabaseLogger" # Optional logger name
1519
+ )
824
1520
  ```
825
1521
 
826
- ### Tool Responses with Errors
1522
+ **Key Features:**
1523
+
1524
+ - 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
1525
+ - Server has capability `logging` to send log messages
1526
+ - Messages are only sent if a transport is configured
1527
+ - Messages are filtered based on the client's configured log level
1528
+ - If the log level hasn't been set by the client, no messages will be sent
1529
+
1530
+ #### Transport Support
827
1531
 
828
- Tools can return error information alongside text content using the `error` parameter.
1532
+ - **stdio**: Notifications are sent as JSON-RPC 2.0 messages to stdout
1533
+ - **Streamable HTTP**: Notifications are sent as JSON-RPC 2.0 messages over HTTP with streaming (chunked transfer or SSE)
829
1534
 
830
- The error will be included in the JSON-RPC response as the `isError` field.
1535
+ #### Usage Example
831
1536
 
832
1537
  ```ruby
833
- class WeatherTool < MCP::Tool
834
- description "Get current weather and return structured data"
1538
+ server = MCP::Server.new(name: "my_server")
835
1539
 
836
- def self.call(server_context:)
837
- # Do something here
838
- content = {}
1540
+ # Default Streamable HTTP - session oriented
1541
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
839
1542
 
840
- MCP::Tool::Response.new(
841
- [{
842
- type: "text",
843
- text: content.to_json
844
- }],
845
- structured_content: content,
846
- error: true
847
- )
848
- end
849
- end
1543
+ # When tools change, notify clients
1544
+ server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
1545
+ server.notify_tools_list_changed
850
1546
  ```
851
1547
 
852
- ### Prompts
853
-
854
- 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.
855
-
856
- The `MCP::Prompt` class provides three ways to create prompts:
857
-
858
- 1. As a class definition with metadata:
1548
+ You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
1549
+ This mode allows for easy multi-node deployment.
1550
+ Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new` (`stateless` defaults to `false`):
859
1551
 
860
1552
  ```ruby
861
- class MyPrompt < MCP::Prompt
862
- prompt_name "my_prompt" # Optional - defaults to underscored class name
863
- title "My Prompt"
864
- description "This prompt performs specific functionality..."
865
- arguments [
866
- MCP::Prompt::Argument.new(
867
- name: "message",
868
- title: "Message Title",
869
- description: "Input message",
870
- required: true
871
- )
872
- ]
873
- meta({ version: "1.0", category: "example" })
874
-
875
- class << self
876
- def template(args, server_context:)
877
- MCP::Prompt::Result.new(
878
- description: "Response description",
879
- messages: [
880
- MCP::Prompt::Message.new(
881
- role: "user",
882
- content: MCP::Content::Text.new("User message")
883
- ),
884
- MCP::Prompt::Message.new(
885
- role: "assistant",
886
- content: MCP::Content::Text.new(args["message"])
887
- )
888
- ]
889
- )
890
- end
891
- end
892
- end
893
-
894
- prompt = MyPrompt
1553
+ # Stateless Streamable HTTP - session-less
1554
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
895
1555
  ```
896
1556
 
897
- 2. Using the `MCP::Prompt.define` method:
1557
+ You can enable JSON response mode, where the server returns `application/json` instead of `text/event-stream`.
1558
+ Set `enable_json_response: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`:
898
1559
 
899
1560
  ```ruby
900
- prompt = MCP::Prompt.define(
901
- name: "my_prompt",
902
- title: "My Prompt",
903
- description: "This prompt performs specific functionality...",
904
- arguments: [
905
- MCP::Prompt::Argument.new(
906
- name: "message",
907
- title: "Message Title",
908
- description: "Input message",
909
- required: true
910
- )
911
- ],
912
- meta: { version: "1.0", category: "example" }
913
- ) do |args, server_context:|
914
- MCP::Prompt::Result.new(
915
- description: "Response description",
916
- messages: [
917
- MCP::Prompt::Message.new(
918
- role: "user",
919
- content: MCP::Content::Text.new("User message")
920
- ),
921
- MCP::Prompt::Message.new(
922
- role: "assistant",
923
- content: MCP::Content::Text.new(args["message"])
924
- )
925
- ]
926
- )
927
- end
1561
+ # JSON response mode
1562
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, enable_json_response: true)
928
1563
  ```
929
1564
 
930
- 3. Using the `MCP::Server#define_prompt` method:
1565
+ In JSON response mode, the POST response is a single JSON object, so server-to-client messages
1566
+ that need to arrive during request processing are not supported:
1567
+ request-scoped notifications (`progress`, `log`) are silently dropped, and all server-to-client requests
1568
+ (`sampling/createMessage`, `roots/list`, `elicitation/create`) raise an error.
1569
+ Session-scoped standalone notifications (`resources/updated`, `elicitation/complete`) and
1570
+ broadcast notifications (`tools/list_changed`, etc.) still flow to clients connected to the GET SSE stream.
1571
+ This mode is suitable for simple tool servers that do not need server-initiated requests.
1572
+
1573
+ By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
1574
+ When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
931
1575
 
932
1576
  ```ruby
933
- server = MCP::Server.new
934
- server.define_prompt(
935
- name: "my_prompt",
936
- description: "This prompt performs specific functionality...",
937
- arguments: [
938
- Prompt::Argument.new(
939
- name: "message",
940
- title: "Message Title",
941
- description: "Input message",
942
- required: true
943
- )
944
- ],
945
- meta: { version: "1.0", category: "example" }
946
- ) do |args, server_context:|
947
- Prompt::Result.new(
948
- description: "Response description",
949
- messages: [
950
- Prompt::Message.new(
951
- role: "user",
952
- content: Content::Text.new("User message")
953
- ),
954
- Prompt::Message.new(
955
- role: "assistant",
956
- content: Content::Text.new(args["message"])
957
- )
958
- ]
959
- )
960
- end
1577
+ # Session timeout of 30 minutes
1578
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
961
1579
  ```
962
1580
 
963
- The server_context parameter is the server_context passed into the server and can be used to pass per request information,
964
- e.g. around authentication state or user preferences.
1581
+ ### Pagination
965
1582
 
966
- ### Key Components
1583
+ The MCP Ruby SDK supports [pagination](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination)
1584
+ for list operations that may return large result sets. Pagination uses string cursor tokens carrying a zero-based offset,
1585
+ treated as opaque by clients: the server decides page size, and the client follows `nextCursor` until the server omits it.
967
1586
 
968
- - `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
969
- - `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
970
- - `MCP::Prompt::Result` - The output of a prompt template containing description and messages
971
- - `MCP::Content::Text` - Text content for messages
1587
+ Pagination applies to `tools/list`, `prompts/list`, `resources/list`, and `resources/templates/list`.
972
1588
 
973
- ### Usage
1589
+ #### Server-Side: Enabling Pagination
974
1590
 
975
- Register prompts with the MCP server:
1591
+ Pass `page_size:` to `MCP::Server.new` to split list responses into pages. When `page_size` is omitted (the default),
1592
+ list responses contain all items in a single response, preserving the pre-pagination behavior.
976
1593
 
977
1594
  ```ruby
978
1595
  server = MCP::Server.new(
979
1596
  name: "my_server",
980
- prompts: [MyPrompt],
981
- server_context: { user_id: current_user.id },
1597
+ tools: tools,
1598
+ page_size: 50,
982
1599
  )
983
1600
  ```
984
1601
 
985
- The server will handle prompt listing and execution through the MCP protocol methods:
986
-
987
- - `prompts/list` - Lists all registered prompts and their schemas
988
- - `prompts/get` - Retrieves and executes a specific prompt with arguments
1602
+ When `page_size` is set, list responses include a `nextCursor` field whenever more pages are available:
989
1603
 
990
- ### Resources
1604
+ ```json
1605
+ {
1606
+ "jsonrpc": "2.0",
1607
+ "id": 1,
1608
+ "result": {
1609
+ "tools": [
1610
+ { "name": "example_tool" }
1611
+ ],
1612
+ "nextCursor": "50"
1613
+ }
1614
+ }
1615
+ ```
991
1616
 
992
- MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
1617
+ 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.
993
1618
 
994
- ### Reading Resources
1619
+ #### Client-Side: Iterating Pages
995
1620
 
996
- The `MCP::Resource` class provides a way to register resources with the server.
1621
+ `MCP::Client` exposes `list_tools`, `list_prompts`, `list_resources`, and `list_resource_templates`.
1622
+ **Each call issues exactly one `*/list` JSON-RPC request and returns exactly one page** — not the full collection.
1623
+ The returned result object (`MCP::Client::ListToolsResult` etc.) exposes the page items and the next cursor as method accessors:
997
1624
 
998
1625
  ```ruby
999
- resource = MCP::Resource.new(
1000
- uri: "https://example.com/my_resource",
1001
- name: "my-resource",
1002
- title: "My Resource",
1003
- description: "Lorem ipsum dolor sit amet",
1004
- mime_type: "text/html",
1005
- )
1626
+ client = MCP::Client.new(transport: transport)
1627
+
1628
+ cursor = nil
1629
+ loop do
1630
+ page = client.list_tools(cursor: cursor)
1631
+ page.tools.each { |tool| process(tool) }
1632
+ cursor = page.next_cursor
1633
+ break unless cursor
1634
+ end
1635
+ ```
1006
1636
 
1007
- server = MCP::Server.new(
1008
- name: "my_server",
1009
- resources: [resource],
1010
- )
1637
+ The same pattern applies to `list_prompts` (`page.prompts`), `list_resources` (`page.resources`), and
1638
+ `list_resource_templates` (`page.resource_templates`). `next_cursor` is `nil` on the final page.
1639
+
1640
+ Because a single call returns a single page, how many items come back depends on the server's `page_size` configuration:
1641
+
1642
+ | Server `page_size` | `client.list_tools(cursor: nil)` |
1643
+ |--------------------|---------------------------------------------------------------------|
1644
+ | Not set (default) | Returns every item in one response. `next_cursor` is `nil`. |
1645
+ | Set to `N` | Returns the first `N` items. `next_cursor` is set for continuation. |
1646
+
1647
+ If your application needs the complete collection regardless of how the server is configured, either loop on
1648
+ `next_cursor` as shown above, or use the whole-collection methods described below.
1649
+
1650
+ #### Fetching the Complete Collection
1651
+
1652
+ `client.tools`, `client.resources`, `client.resource_templates`, and `client.prompts` auto-iterate
1653
+ through all pages and return a plain array of items, guaranteeing the full collection regardless
1654
+ of the server's `page_size` setting. When a server paginates, they issue multiple JSON-RPC round
1655
+ trips per call and break out of the pagination loop if the server returns the same `nextCursor`
1656
+ twice in a row as a safety measure.
1657
+
1658
+ ```ruby
1659
+ tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
1011
1660
  ```
1012
1661
 
1013
- The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
1662
+ Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
1663
+ fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
1664
+
1665
+ ### Advanced
1666
+
1667
+ #### Custom Methods
1668
+
1669
+ The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
1014
1670
 
1015
1671
  ```ruby
1016
- server.resources_read_handler do |params|
1017
- [{
1018
- uri: params[:uri],
1019
- mimeType: "text/plain",
1020
- text: "Hello from example resource! URI: #{params[:uri]}"
1021
- }]
1672
+ server = MCP::Server.new(name: "my_server")
1673
+
1674
+ # Define a custom method that returns a result
1675
+ server.define_custom_method(method_name: "add") do |params|
1676
+ params[:a] + params[:b]
1677
+ end
1678
+
1679
+ # Define a custom notification method (returns nil)
1680
+ server.define_custom_method(method_name: "notify") do |params|
1681
+ # Process notification
1682
+ nil
1022
1683
  end
1023
1684
  ```
1024
1685
 
1025
- otherwise `resources/read` requests will be a no-op.
1686
+ **Key Features:**
1026
1687
 
1027
- ### Resource Templates
1688
+ - Accepts any method name as a string
1689
+ - Block receives the request parameters as a hash
1690
+ - Can handle both regular methods (with responses) and notifications
1691
+ - Prevents overriding existing MCP protocol methods
1692
+ - Supports instrumentation callbacks for monitoring
1028
1693
 
1029
- The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
1694
+ **Usage Example:**
1030
1695
 
1031
1696
  ```ruby
1032
- resource_template = MCP::ResourceTemplate.new(
1033
- uri_template: "https://example.com/my_resource_template",
1034
- name: "my-resource-template",
1035
- title: "My Resource Template",
1036
- description: "Lorem ipsum dolor sit amet",
1037
- mime_type: "text/html",
1038
- )
1697
+ # Client request
1698
+ {
1699
+ "jsonrpc": "2.0",
1700
+ "id": 1,
1701
+ "method": "add",
1702
+ "params": { "a": 5, "b": 3 }
1703
+ }
1039
1704
 
1040
- server = MCP::Server.new(
1041
- name: "my_server",
1042
- resource_templates: [resource_template],
1043
- )
1705
+ # Server response
1706
+ {
1707
+ "jsonrpc": "2.0",
1708
+ "id": 1,
1709
+ "result": 8
1710
+ }
1044
1711
  ```
1045
1712
 
1713
+ **Error Handling:**
1714
+
1715
+ - Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
1716
+ - Supports the same exception reporting and instrumentation as standard methods
1717
+
1046
1718
  ## Building an MCP Client
1047
1719
 
1048
1720
  The `MCP::Client` class provides an interface for interacting with MCP servers.
1049
1721
 
1050
1722
  This class supports:
1051
1723
 
1724
+ - Liveness check via the `ping` method (`MCP::Client#ping`)
1052
1725
  - Tool listing via the `tools/list` method (`MCP::Client#tools`)
1053
1726
  - Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
1054
1727
  - Resource listing via the `resources/list` method (`MCP::Client#resources`)
1055
1728
  - Resource template listing via the `resources/templates/list` method (`MCP::Client#resource_templates`)
1056
- - Resource reading via the `resources/read` method (`MCP::Client#read_resources`)
1729
+ - Resource reading via the `resources/read` method (`MCP::Client#read_resource`)
1057
1730
  - Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
1058
1731
  - Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
1732
+ - Completion requests via the `completion/complete` method (`MCP::Client#complete`)
1059
1733
  - Automatic JSON-RPC 2.0 message formatting
1060
1734
  - UUID request ID generation
1061
1735
 
@@ -1108,6 +1782,9 @@ stdio_transport = MCP::Client::Stdio.new(
1108
1782
  )
1109
1783
  client = MCP::Client.new(transport: stdio_transport)
1110
1784
 
1785
+ # Perform the MCP initialization handshake before sending any requests.
1786
+ client.connect
1787
+
1111
1788
  # List available tools.
1112
1789
  tools = client.tools
1113
1790
  tools.each do |tool|
@@ -1134,11 +1811,12 @@ The stdio transport automatically handles:
1134
1811
 
1135
1812
  Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
1136
1813
 
1137
- You'll need to add `faraday` as a dependency in order to use the HTTP transport layer:
1814
+ 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:
1138
1815
 
1139
1816
  ```ruby
1140
1817
  gem 'mcp'
1141
1818
  gem 'faraday', '>= 2.0'
1819
+ gem 'event_stream_parser', '>= 1.0' # optional, required only for SSE responses
1142
1820
  ```
1143
1821
 
1144
1822
  Example usage:
@@ -1147,6 +1825,9 @@ Example usage:
1147
1825
  http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
1148
1826
  client = MCP::Client.new(transport: http_transport)
1149
1827
 
1828
+ # Perform the MCP initialization handshake before sending any requests.
1829
+ client.connect
1830
+
1150
1831
  # List available tools
1151
1832
  tools = client.tools
1152
1833
  tools.each do |tool|
@@ -1191,6 +1872,18 @@ client.tools # will make the call using Bearer auth
1191
1872
 
1192
1873
  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.
1193
1874
 
1875
+ #### Customizing the Faraday Connection
1876
+
1877
+ You can pass a block to `MCP::Client::HTTP.new` to customize the underlying Faraday connection.
1878
+ The block is called after the default middleware is configured, so you can add middleware or swap the HTTP adapter:
1879
+
1880
+ ```ruby
1881
+ http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp") do |faraday|
1882
+ faraday.use MyApp::Middleware::HttpRecorder
1883
+ faraday.adapter :typhoeus
1884
+ end
1885
+ ```
1886
+
1194
1887
  ### Tool Objects
1195
1888
 
1196
1889
  The client provides a wrapper class for tools returned by the server: