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