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