mcp 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +467 -131
- data/lib/json_rpc_handler.rb +16 -9
- data/lib/mcp/client/http.rb +133 -14
- data/lib/mcp/client/paginated_result.rb +13 -0
- data/lib/mcp/client.rb +195 -22
- 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/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/streamable_http_transport.rb +35 -8
- data/lib/mcp/server.rb +82 -60
- data/lib/mcp/server_context.rb +54 -0
- data/lib/mcp/server_session.rb +45 -0
- data/lib/mcp/tool/response.rb +4 -3
- data/lib/mcp/version.rb +1 -1
- metadata +6 -4
data/README.md
CHANGED
|
@@ -38,7 +38,9 @@ It implements the Model Context Protocol specification, handling model context r
|
|
|
38
38
|
- Supports resource registration and retrieval
|
|
39
39
|
- Supports stdio & Streamable HTTP (including SSE) transports
|
|
40
40
|
- Supports notifications for list changes (tools, prompts, resources)
|
|
41
|
+
- Supports roots (server-to-client filesystem boundary queries)
|
|
41
42
|
- Supports sampling (server-to-client LLM completion requests)
|
|
43
|
+
- Supports cursor-based pagination for list operations
|
|
42
44
|
|
|
43
45
|
### Supported Methods
|
|
44
46
|
|
|
@@ -51,8 +53,12 @@ It implements the Model Context Protocol specification, handling model context r
|
|
|
51
53
|
- `resources/list` - Lists all registered resources and their schemas
|
|
52
54
|
- `resources/read` - Retrieves a specific resource by name
|
|
53
55
|
- `resources/templates/list` - Lists all registered resource templates and their schemas
|
|
56
|
+
- `resources/subscribe` - Subscribes to updates for a specific resource
|
|
57
|
+
- `resources/unsubscribe` - Unsubscribes from updates for a specific resource
|
|
54
58
|
- `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
|
|
59
|
+
- `roots/list` - Requests filesystem roots from the client (server-to-client)
|
|
55
60
|
- `sampling/createMessage` - Requests LLM completion from the client (server-to-client)
|
|
61
|
+
- `elicitation/create` - Requests user input from the client (server-to-client)
|
|
56
62
|
|
|
57
63
|
### Usage
|
|
58
64
|
|
|
@@ -103,14 +109,56 @@ $ ruby examples/stdio_server.rb
|
|
|
103
109
|
{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
|
|
104
110
|
```
|
|
105
111
|
|
|
106
|
-
####
|
|
112
|
+
#### Streamable HTTP Transport
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
requests.
|
|
114
|
+
`MCP::Server::Transports::StreamableHTTPTransport` is a standard Rack app, so it can be mounted in any Rack-compatible framework.
|
|
115
|
+
The following examples show two common integration styles in Rails.
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
> [!IMPORTANT]
|
|
118
|
+
> `MCP::Server::Transports::StreamableHTTPTransport` stores session and SSE stream state in memory,
|
|
119
|
+
> so it must run in a single process. Use a single-process server (e.g., Puma with `workers 0`).
|
|
120
|
+
> Multi-process configurations (Unicorn, or Puma with `workers > 0`) fork separate processes that
|
|
121
|
+
> do not share memory, which breaks session management and SSE connections.
|
|
122
|
+
>
|
|
123
|
+
> When running multiple server instances behind a load balancer, configure your load balancer to use
|
|
124
|
+
> sticky sessions (session affinity) so that requests with the same `Mcp-Session-Id` header are always
|
|
125
|
+
> routed to the same instance.
|
|
126
|
+
>
|
|
127
|
+
> Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
|
|
128
|
+
|
|
129
|
+
##### Rails (mount)
|
|
130
|
+
|
|
131
|
+
`StreamableHTTPTransport` is a Rack app that can be mounted directly in Rails routes:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# config/routes.rb
|
|
135
|
+
server = MCP::Server.new(
|
|
136
|
+
name: "my_server",
|
|
137
|
+
title: "Example Server Display Name",
|
|
138
|
+
version: "1.0.0",
|
|
139
|
+
instructions: "Use the tools of this server as a last resort",
|
|
140
|
+
tools: [SomeTool, AnotherTool],
|
|
141
|
+
prompts: [MyPrompt],
|
|
142
|
+
)
|
|
143
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
144
|
+
|
|
145
|
+
Rails.application.routes.draw do
|
|
146
|
+
mount transport => "/mcp"
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`mount` directs all HTTP methods on `/mcp` to the transport. `StreamableHTTPTransport` internally dispatches
|
|
151
|
+
`POST` (client-to-server JSON-RPC messages, with responses optionally streamed via SSE),
|
|
152
|
+
`GET` (optional standalone SSE stream for server-to-client messages), and `DELETE` (session termination) per
|
|
153
|
+
the [MCP Streamable HTTP transport spec](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http),
|
|
154
|
+
so no additional route configuration is needed.
|
|
155
|
+
|
|
156
|
+
##### Rails (controller)
|
|
157
|
+
|
|
158
|
+
While the mount approach creates a single server at boot time, the controller approach creates a new server per request.
|
|
159
|
+
This allows you to customize tools, prompts, or configuration based on the request (e.g., different tools per route).
|
|
160
|
+
|
|
161
|
+
`StreamableHTTPTransport#handle_request` returns proper HTTP status codes (e.g., 202 Accepted for notifications):
|
|
114
162
|
|
|
115
163
|
```ruby
|
|
116
164
|
class McpController < ActionController::API
|
|
@@ -133,18 +181,6 @@ class McpController < ActionController::API
|
|
|
133
181
|
end
|
|
134
182
|
```
|
|
135
183
|
|
|
136
|
-
> [!IMPORTANT]
|
|
137
|
-
> `MCP::Server::Transports::StreamableHTTPTransport` stores session and SSE stream state in memory,
|
|
138
|
-
> so it must run in a single process. Use a single-process server (e.g., Puma with `workers 0`).
|
|
139
|
-
> Multi-process configurations (Unicorn, or Puma with `workers > 0`) fork separate processes that
|
|
140
|
-
> do not share memory, which breaks session management and SSE connections.
|
|
141
|
-
>
|
|
142
|
-
> When running multiple server instances behind a load balancer, configure your load balancer to use
|
|
143
|
-
> sticky sessions (session affinity) so that requests with the same `Mcp-Session-Id` header are always
|
|
144
|
-
> routed to the same instance.
|
|
145
|
-
>
|
|
146
|
-
> Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
|
|
147
|
-
|
|
148
184
|
### Configuration
|
|
149
185
|
|
|
150
186
|
The gem can be configured using the `MCP.configure` block:
|
|
@@ -159,15 +195,17 @@ MCP.configure do |config|
|
|
|
159
195
|
end
|
|
160
196
|
}
|
|
161
197
|
|
|
162
|
-
config.
|
|
163
|
-
|
|
198
|
+
config.around_request = ->(data, &request_handler) {
|
|
199
|
+
logger.info("Start: #{data[:method]}")
|
|
200
|
+
request_handler.call
|
|
201
|
+
logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
|
|
164
202
|
}
|
|
165
203
|
end
|
|
166
204
|
```
|
|
167
205
|
|
|
168
206
|
or by creating an explicit configuration and passing it into the server.
|
|
169
207
|
This is useful for systems where an application hosts more than one MCP server but
|
|
170
|
-
they might require different
|
|
208
|
+
they might require different configurations.
|
|
171
209
|
|
|
172
210
|
```ruby
|
|
173
211
|
configuration = MCP::Configuration.new
|
|
@@ -179,8 +217,10 @@ configuration.exception_reporter = ->(exception, server_context) {
|
|
|
179
217
|
end
|
|
180
218
|
}
|
|
181
219
|
|
|
182
|
-
configuration.
|
|
183
|
-
|
|
220
|
+
configuration.around_request = ->(data, &request_handler) {
|
|
221
|
+
logger.info("Start: #{data[:method]}")
|
|
222
|
+
request_handler.call
|
|
223
|
+
logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
|
|
184
224
|
}
|
|
185
225
|
|
|
186
226
|
server = MCP::Server.new(
|
|
@@ -193,7 +233,8 @@ server = MCP::Server.new(
|
|
|
193
233
|
|
|
194
234
|
#### `server_context`
|
|
195
235
|
|
|
196
|
-
The `server_context` is a user-defined hash that is passed into the server instance and made available to
|
|
236
|
+
The `server_context` is a user-defined hash that is passed into the server instance and made available to tool and prompt calls.
|
|
237
|
+
It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
|
|
197
238
|
|
|
198
239
|
**Type:**
|
|
199
240
|
|
|
@@ -210,7 +251,9 @@ server = MCP::Server.new(
|
|
|
210
251
|
)
|
|
211
252
|
```
|
|
212
253
|
|
|
213
|
-
This hash is then passed as the `server_context` argument to tool and prompt calls
|
|
254
|
+
This hash is then passed as the `server_context` keyword argument to tool and prompt calls.
|
|
255
|
+
Note that exception and instrumentation callbacks do not receive this user-defined hash.
|
|
256
|
+
See the relevant sections below for the arguments they receive.
|
|
214
257
|
|
|
215
258
|
#### Request-specific `_meta` Parameter
|
|
216
259
|
|
|
@@ -263,7 +306,9 @@ end
|
|
|
263
306
|
The exception reporter receives:
|
|
264
307
|
|
|
265
308
|
- `exception`: The Ruby exception object that was raised
|
|
266
|
-
- `server_context`:
|
|
309
|
+
- `server_context`: A hash describing where the failure occurred (e.g., `{ request: <raw JSON-RPC request> }`
|
|
310
|
+
for request handling, `{ notification: "tools_list_changed" }` for notification delivery).
|
|
311
|
+
This is not the user-defined `server_context` passed to `Server.new`.
|
|
267
312
|
|
|
268
313
|
**Signature:**
|
|
269
314
|
|
|
@@ -271,9 +316,67 @@ The exception reporter receives:
|
|
|
271
316
|
exception_reporter = ->(exception, server_context) { ... }
|
|
272
317
|
```
|
|
273
318
|
|
|
274
|
-
#####
|
|
319
|
+
##### Around Request
|
|
320
|
+
|
|
321
|
+
The `around_request` hook wraps request handling, allowing you to execute code before and after each request.
|
|
322
|
+
This is useful for Application Performance Monitoring (APM) tracing, logging, or other observability needs.
|
|
323
|
+
|
|
324
|
+
The hook receives a `data` hash and a `request_handler` block. You must call `request_handler.call` to execute the request:
|
|
325
|
+
|
|
326
|
+
**Signature:**
|
|
275
327
|
|
|
276
|
-
|
|
328
|
+
```ruby
|
|
329
|
+
around_request = ->(data, &request_handler) { request_handler.call }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**`data` availability by timing:**
|
|
333
|
+
|
|
334
|
+
- Before `request_handler.call`: `method`
|
|
335
|
+
- After `request_handler.call`: `tool_name`, `tool_arguments`, `prompt_name`, `resource_uri`, `error`, `client`
|
|
336
|
+
- Not available inside `around_request`: `duration` (added after `around_request` returns)
|
|
337
|
+
|
|
338
|
+
> [!NOTE]
|
|
339
|
+
> `tool_name`, `prompt_name` and `resource_uri` may only be populated for the corresponding request methods
|
|
340
|
+
> (`tools/call`, `prompts/get`, `resources/read`), and may not be set depending on how the request is handled
|
|
341
|
+
> (for example, `prompt_name` is not recorded when the prompt is not found).
|
|
342
|
+
> `duration` is added after `around_request` returns, so it is not visible from within the hook.
|
|
343
|
+
|
|
344
|
+
**Example:**
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
MCP.configure do |config|
|
|
348
|
+
config.around_request = ->(data, &request_handler) {
|
|
349
|
+
logger.info("Start: #{data[:method]}")
|
|
350
|
+
request_handler.call
|
|
351
|
+
logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
|
|
352
|
+
}
|
|
353
|
+
end
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
##### Instrumentation Callback (soft-deprecated)
|
|
357
|
+
|
|
358
|
+
> [!NOTE]
|
|
359
|
+
> `instrumentation_callback` is soft-deprecated. Use `around_request` instead.
|
|
360
|
+
>
|
|
361
|
+
> To migrate, wrap the call in `begin/ensure` so the callback still runs when the request fails:
|
|
362
|
+
>
|
|
363
|
+
> ```ruby
|
|
364
|
+
> # Before
|
|
365
|
+
> config.instrumentation_callback = ->(data) { log(data) }
|
|
366
|
+
>
|
|
367
|
+
> # After
|
|
368
|
+
> config.around_request = ->(data, &request_handler) do
|
|
369
|
+
> request_handler.call
|
|
370
|
+
> ensure
|
|
371
|
+
> log(data)
|
|
372
|
+
> end
|
|
373
|
+
> ```
|
|
374
|
+
>
|
|
375
|
+
> Note that `data[:duration]` is not available inside `around_request`.
|
|
376
|
+
> If you need it, measure elapsed time yourself within the hook, or keep using `instrumentation_callback`.
|
|
377
|
+
|
|
378
|
+
The instrumentation callback is called after each request finishes, whether successfully or with an error.
|
|
379
|
+
It receives a hash with the following possible keys:
|
|
277
380
|
|
|
278
381
|
- `method`: (String) The protocol method called (e.g., "ping", "tools/list")
|
|
279
382
|
- `tool_name`: (String, optional) The name of the tool called
|
|
@@ -284,25 +387,10 @@ The instrumentation callback receives a hash with the following possible keys:
|
|
|
284
387
|
- `duration`: (Float) Duration of the call in seconds
|
|
285
388
|
- `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
|
|
286
389
|
|
|
287
|
-
|
|
288
|
-
> `tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
|
|
289
|
-
> This is to avoid potential issues with metric cardinality.
|
|
290
|
-
|
|
291
|
-
**Type:**
|
|
390
|
+
**Signature:**
|
|
292
391
|
|
|
293
392
|
```ruby
|
|
294
393
|
instrumentation_callback = ->(data) { ... }
|
|
295
|
-
# where data is a Hash with keys as described above
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
**Example:**
|
|
299
|
-
|
|
300
|
-
```ruby
|
|
301
|
-
MCP.configure do |config|
|
|
302
|
-
config.instrumentation_callback = ->(data) {
|
|
303
|
-
puts "Instrumentation: #{data.inspect}"
|
|
304
|
-
}
|
|
305
|
-
end
|
|
306
394
|
```
|
|
307
395
|
|
|
308
396
|
### Server Protocol Version
|
|
@@ -808,6 +896,108 @@ server = MCP::Server.new(
|
|
|
808
896
|
)
|
|
809
897
|
```
|
|
810
898
|
|
|
899
|
+
### Roots
|
|
900
|
+
|
|
901
|
+
The Model Context Protocol allows servers to request filesystem roots from clients through the `roots/list` method.
|
|
902
|
+
Roots define the boundaries of where a server can operate, providing a list of directories and files the client has made available.
|
|
903
|
+
|
|
904
|
+
**Key Concepts:**
|
|
905
|
+
|
|
906
|
+
- **Server-to-Client Request**: Like sampling, roots listing is initiated by the server
|
|
907
|
+
- **Client Capability**: Clients must declare `roots` capability during initialization
|
|
908
|
+
- **Change Notifications**: Clients that support `roots.listChanged` send `notifications/roots/list_changed` when roots change
|
|
909
|
+
|
|
910
|
+
**Using Roots in Tools:**
|
|
911
|
+
|
|
912
|
+
Tools that accept a `server_context:` parameter can call `list_roots` on it.
|
|
913
|
+
The request is automatically routed to the correct client session:
|
|
914
|
+
|
|
915
|
+
```ruby
|
|
916
|
+
class FileSearchTool < MCP::Tool
|
|
917
|
+
description "Search files within the client's project roots"
|
|
918
|
+
input_schema(
|
|
919
|
+
properties: {
|
|
920
|
+
query: { type: "string" }
|
|
921
|
+
},
|
|
922
|
+
required: ["query"]
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
def self.call(query:, server_context:)
|
|
926
|
+
roots = server_context.list_roots
|
|
927
|
+
root_uris = roots[:roots].map { |root| root[:uri] }
|
|
928
|
+
|
|
929
|
+
MCP::Tool::Response.new([{
|
|
930
|
+
type: "text",
|
|
931
|
+
text: "Searching in roots: #{root_uris.join(", ")}"
|
|
932
|
+
}])
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
Result contains an array of root objects:
|
|
938
|
+
|
|
939
|
+
```ruby
|
|
940
|
+
{
|
|
941
|
+
roots: [
|
|
942
|
+
{ uri: "file:///home/user/projects/myproject", name: "My Project" },
|
|
943
|
+
{ uri: "file:///home/user/repos/backend", name: "Backend Repository" }
|
|
944
|
+
]
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**Handling Root Changes:**
|
|
949
|
+
|
|
950
|
+
Register a callback to be notified when the client's roots change:
|
|
951
|
+
|
|
952
|
+
```ruby
|
|
953
|
+
server.roots_list_changed_handler do
|
|
954
|
+
puts "Client's roots have changed, tools will see updated roots on next call."
|
|
955
|
+
end
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
**Error Handling:**
|
|
959
|
+
|
|
960
|
+
- Raises `RuntimeError` if client does not support `roots` capability
|
|
961
|
+
- Raises `StandardError` if client returns an error response
|
|
962
|
+
|
|
963
|
+
### Resource Subscriptions
|
|
964
|
+
|
|
965
|
+
Resource subscriptions allow clients to monitor specific resources for changes.
|
|
966
|
+
When a subscribed resource is updated, the server sends a notification to the client.
|
|
967
|
+
|
|
968
|
+
The SDK does not track subscription state internally.
|
|
969
|
+
Server developers register handlers and manage their own subscription state.
|
|
970
|
+
Three methods are provided:
|
|
971
|
+
|
|
972
|
+
- `Server#resources_subscribe_handler` - registers a handler for `resources/subscribe` requests
|
|
973
|
+
- `Server#resources_unsubscribe_handler` - registers a handler for `resources/unsubscribe` requests
|
|
974
|
+
- `ServerContext#notify_resources_updated` - sends a `notifications/resources/updated` notification to the subscribing client
|
|
975
|
+
|
|
976
|
+
```ruby
|
|
977
|
+
subscribed_uris = Set.new
|
|
978
|
+
|
|
979
|
+
server = MCP::Server.new(
|
|
980
|
+
name: "my_server",
|
|
981
|
+
resources: [my_resource],
|
|
982
|
+
capabilities: { resources: { subscribe: true } },
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
server.resources_subscribe_handler do |params|
|
|
986
|
+
subscribed_uris.add(params[:uri].to_s)
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
server.resources_unsubscribe_handler do |params|
|
|
990
|
+
subscribed_uris.delete(params[:uri].to_s)
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
server.define_tool(name: "update_resource") do |server_context:, **args|
|
|
994
|
+
if subscribed_uris.include?("test://my-resource")
|
|
995
|
+
server_context.notify_resources_updated(uri: "test://my-resource")
|
|
996
|
+
end
|
|
997
|
+
MCP::Tool::Response.new([MCP::Content::Text.new("Resource updated").to_h])
|
|
998
|
+
end
|
|
999
|
+
```
|
|
1000
|
+
|
|
811
1001
|
### Sampling
|
|
812
1002
|
|
|
813
1003
|
The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.
|
|
@@ -823,8 +1013,7 @@ This enables servers to leverage the client's LLM capabilities without needing d
|
|
|
823
1013
|
**Using Sampling in Tools:**
|
|
824
1014
|
|
|
825
1015
|
Tools that accept a `server_context:` parameter can call `create_sampling_message` on it.
|
|
826
|
-
The request is automatically routed to the correct client session
|
|
827
|
-
Set `server.server_context = server` so that `server_context.create_sampling_message` delegates to the server:
|
|
1016
|
+
The request is automatically routed to the correct client session:
|
|
828
1017
|
|
|
829
1018
|
```ruby
|
|
830
1019
|
class SummarizeTool < MCP::Tool
|
|
@@ -852,7 +1041,6 @@ class SummarizeTool < MCP::Tool
|
|
|
852
1041
|
end
|
|
853
1042
|
|
|
854
1043
|
server = MCP::Server.new(name: "my_server", tools: [SummarizeTool])
|
|
855
|
-
server.server_context = server
|
|
856
1044
|
```
|
|
857
1045
|
|
|
858
1046
|
**Parameters:**
|
|
@@ -873,86 +1061,8 @@ Optional:
|
|
|
873
1061
|
- `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
|
|
874
1062
|
- `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)
|
|
875
1063
|
|
|
876
|
-
**Direct Usage:**
|
|
877
|
-
|
|
878
|
-
`Server#create_sampling_message` can also be called directly outside of tools:
|
|
879
|
-
|
|
880
|
-
```ruby
|
|
881
|
-
result = server.create_sampling_message(
|
|
882
|
-
messages: [
|
|
883
|
-
{ role: "user", content: { type: "text", text: "What is the capital of France?" } }
|
|
884
|
-
],
|
|
885
|
-
max_tokens: 100,
|
|
886
|
-
system_prompt: "You are a helpful assistant.",
|
|
887
|
-
temperature: 0.7
|
|
888
|
-
)
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
Result contains the LLM response:
|
|
892
|
-
|
|
893
|
-
```ruby
|
|
894
|
-
{
|
|
895
|
-
role: "assistant",
|
|
896
|
-
content: { type: "text", text: "The capital of France is Paris." },
|
|
897
|
-
model: "claude-3-sonnet-20240307",
|
|
898
|
-
stopReason: "endTurn"
|
|
899
|
-
}
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
For multi-client transports (e.g., `StreamableHTTPTransport`), use `server_context.create_sampling_message` inside tools
|
|
903
|
-
to route the request to the correct client session.
|
|
904
|
-
|
|
905
|
-
**Tool Use in Sampling:**
|
|
906
|
-
|
|
907
|
-
When tools are provided in a sampling request, the LLM can call them during generation.
|
|
908
|
-
The server must handle tool calls and continue the conversation with tool results:
|
|
909
|
-
|
|
910
|
-
```ruby
|
|
911
|
-
result = server.create_sampling_message(
|
|
912
|
-
messages: [
|
|
913
|
-
{ role: "user", content: { type: "text", text: "What's the weather in Paris?" } }
|
|
914
|
-
],
|
|
915
|
-
max_tokens: 1000,
|
|
916
|
-
tools: [
|
|
917
|
-
{
|
|
918
|
-
name: "get_weather",
|
|
919
|
-
description: "Get weather for a city",
|
|
920
|
-
inputSchema: {
|
|
921
|
-
type: "object",
|
|
922
|
-
properties: { city: { type: "string" } },
|
|
923
|
-
required: ["city"]
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
],
|
|
927
|
-
tool_choice: { mode: "auto" }
|
|
928
|
-
)
|
|
929
|
-
|
|
930
|
-
if result[:stopReason] == "toolUse"
|
|
931
|
-
tool_results = result[:content].map do |tool_use|
|
|
932
|
-
weather_data = get_weather(tool_use[:input][:city])
|
|
933
|
-
|
|
934
|
-
{
|
|
935
|
-
type: "tool_result",
|
|
936
|
-
toolUseId: tool_use[:id],
|
|
937
|
-
content: [{ type: "text", text: weather_data.to_json }]
|
|
938
|
-
}
|
|
939
|
-
end
|
|
940
|
-
|
|
941
|
-
final_result = server.create_sampling_message(
|
|
942
|
-
messages: [
|
|
943
|
-
{ role: "user", content: { type: "text", text: "What's the weather in Paris?" } },
|
|
944
|
-
{ role: "assistant", content: result[:content] },
|
|
945
|
-
{ role: "user", content: tool_results }
|
|
946
|
-
],
|
|
947
|
-
max_tokens: 1000,
|
|
948
|
-
tools: [...]
|
|
949
|
-
)
|
|
950
|
-
end
|
|
951
|
-
```
|
|
952
|
-
|
|
953
1064
|
**Error Handling:**
|
|
954
1065
|
|
|
955
|
-
- Raises `RuntimeError` if transport is not set
|
|
956
1066
|
- Raises `RuntimeError` if client does not support `sampling` capability
|
|
957
1067
|
- Raises `RuntimeError` if `tools` are used but client lacks `sampling.tools` capability
|
|
958
1068
|
- Raises `StandardError` if client returns an error response
|
|
@@ -989,6 +1099,33 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
|
|
|
989
1099
|
- `notifications/progress`
|
|
990
1100
|
- `notifications/message`
|
|
991
1101
|
|
|
1102
|
+
### Ping
|
|
1103
|
+
|
|
1104
|
+
The MCP Ruby SDK supports the
|
|
1105
|
+
[MCP `ping` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping),
|
|
1106
|
+
which allows either side of the connection to verify that the peer is still responsive.
|
|
1107
|
+
A `ping` request has no parameters, and the receiver MUST respond promptly with an empty result.
|
|
1108
|
+
|
|
1109
|
+
#### Server-Side
|
|
1110
|
+
|
|
1111
|
+
Servers respond to incoming `ping` requests automatically - no setup is required.
|
|
1112
|
+
Any `MCP::Server` instance replies with an empty result.
|
|
1113
|
+
|
|
1114
|
+
#### Client-Side
|
|
1115
|
+
|
|
1116
|
+
`MCP::Client` exposes `ping` to send a ping to the server:
|
|
1117
|
+
|
|
1118
|
+
```ruby
|
|
1119
|
+
client = MCP::Client.new(transport: transport)
|
|
1120
|
+
client.ping # => {} on success
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
`#ping` raises `MCP::Client::ServerError` when the server returns a JSON-RPC error.
|
|
1124
|
+
It raises `MCP::Client::ValidationError` when the response `result` is missing or
|
|
1125
|
+
is not a Hash (matching the spec requirement that `result` be an object).
|
|
1126
|
+
Transport-level errors (for example, `MCP::Client::Stdio`'s `read_timeout:` firing)
|
|
1127
|
+
propagate as exceptions raised by the transport layer.
|
|
1128
|
+
|
|
992
1129
|
### Progress
|
|
993
1130
|
|
|
994
1131
|
The MCP Ruby SDK supports progress tracking for long-running tool operations,
|
|
@@ -1085,6 +1222,108 @@ The SDK automatically enforces the 100-item limit per the MCP specification.
|
|
|
1085
1222
|
The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
|
|
1086
1223
|
Requests for unknown references return an error.
|
|
1087
1224
|
|
|
1225
|
+
### Elicitation
|
|
1226
|
+
|
|
1227
|
+
The MCP Ruby SDK supports [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation),
|
|
1228
|
+
which allows servers to request additional information from users through the client during tool execution.
|
|
1229
|
+
|
|
1230
|
+
Elicitation is a **server-to-client request**. The server sends a request and blocks until the user responds via the client.
|
|
1231
|
+
|
|
1232
|
+
#### Capabilities
|
|
1233
|
+
|
|
1234
|
+
Clients must declare the `elicitation` capability during initialization. The server checks this before sending any elicitation request
|
|
1235
|
+
and raises a `RuntimeError` if the client does not support it.
|
|
1236
|
+
|
|
1237
|
+
For URL mode support, the client must also declare `elicitation.url` capability.
|
|
1238
|
+
|
|
1239
|
+
#### Using Elicitation in Tools
|
|
1240
|
+
|
|
1241
|
+
Tools that accept a `server_context:` parameter can call `create_form_elicitation` on it:
|
|
1242
|
+
|
|
1243
|
+
```ruby
|
|
1244
|
+
server.define_tool(name: "collect_info", description: "Collect user info") do |server_context:|
|
|
1245
|
+
result = server_context.create_form_elicitation(
|
|
1246
|
+
message: "Please provide your name",
|
|
1247
|
+
requested_schema: {
|
|
1248
|
+
type: "object",
|
|
1249
|
+
properties: { name: { type: "string" } },
|
|
1250
|
+
required: ["name"],
|
|
1251
|
+
},
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
MCP::Tool::Response.new([{ type: "text", text: "Hello, #{result[:content][:name]}" }])
|
|
1255
|
+
end
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
#### Form Mode
|
|
1259
|
+
|
|
1260
|
+
Form mode collects structured data from the user directly through the MCP client:
|
|
1261
|
+
|
|
1262
|
+
```ruby
|
|
1263
|
+
server.define_tool(name: "collect_contact", description: "Collect contact info") do |server_context:|
|
|
1264
|
+
result = server_context.create_form_elicitation(
|
|
1265
|
+
message: "Please provide your contact information",
|
|
1266
|
+
requested_schema: {
|
|
1267
|
+
type: "object",
|
|
1268
|
+
properties: {
|
|
1269
|
+
name: { type: "string", description: "Your full name" },
|
|
1270
|
+
email: { type: "string", format: "email", description: "Your email address" },
|
|
1271
|
+
},
|
|
1272
|
+
required: ["name", "email"],
|
|
1273
|
+
},
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
text = case result[:action]
|
|
1277
|
+
when "accept"
|
|
1278
|
+
"Hello, #{result[:content][:name]} (#{result[:content][:email]})"
|
|
1279
|
+
when "decline"
|
|
1280
|
+
"User declined"
|
|
1281
|
+
when "cancel"
|
|
1282
|
+
"User cancelled"
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
MCP::Tool::Response.new([{ type: "text", text: text }])
|
|
1286
|
+
end
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
#### URL Mode
|
|
1290
|
+
|
|
1291
|
+
URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
|
|
1292
|
+
|
|
1293
|
+
```ruby
|
|
1294
|
+
server.define_tool(name: "authorize_github", description: "Authorize GitHub") do |server_context:|
|
|
1295
|
+
elicitation_id = SecureRandom.uuid
|
|
1296
|
+
|
|
1297
|
+
result = server_context.create_url_elicitation(
|
|
1298
|
+
message: "Please authorize access to your GitHub account",
|
|
1299
|
+
url: "https://example.com/oauth/authorize?elicitation_id=#{elicitation_id}",
|
|
1300
|
+
elicitation_id: elicitation_id,
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
server_context.notify_elicitation_complete(elicitation_id: elicitation_id)
|
|
1304
|
+
|
|
1305
|
+
MCP::Tool::Response.new([{ type: "text", text: "Authorization complete" }])
|
|
1306
|
+
end
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
#### URLElicitationRequiredError
|
|
1310
|
+
|
|
1311
|
+
When a tool cannot proceed until an out-of-band elicitation is completed, raise `MCP::Server::URLElicitationRequiredError`.
|
|
1312
|
+
This returns a JSON-RPC error with code `-32042` to the client:
|
|
1313
|
+
|
|
1314
|
+
```ruby
|
|
1315
|
+
server.define_tool(name: "access_github", description: "Access GitHub") do |server_context:|
|
|
1316
|
+
raise MCP::Server::URLElicitationRequiredError.new([
|
|
1317
|
+
{
|
|
1318
|
+
mode: "url",
|
|
1319
|
+
elicitationId: SecureRandom.uuid,
|
|
1320
|
+
url: "https://example.com/oauth/authorize",
|
|
1321
|
+
message: "GitHub authorization is required.",
|
|
1322
|
+
},
|
|
1323
|
+
])
|
|
1324
|
+
end
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1088
1327
|
### Logging
|
|
1089
1328
|
|
|
1090
1329
|
The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
|
|
@@ -1186,6 +1425,22 @@ Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`
|
|
|
1186
1425
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
|
|
1187
1426
|
```
|
|
1188
1427
|
|
|
1428
|
+
You can enable JSON response mode, where the server returns `application/json` instead of `text/event-stream`.
|
|
1429
|
+
Set `enable_json_response: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`:
|
|
1430
|
+
|
|
1431
|
+
```ruby
|
|
1432
|
+
# JSON response mode
|
|
1433
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, enable_json_response: true)
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
In JSON response mode, the POST response is a single JSON object, so server-to-client messages
|
|
1437
|
+
that need to arrive during request processing are not supported:
|
|
1438
|
+
request-scoped notifications (`progress`, `log`) are silently dropped, and all server-to-client requests
|
|
1439
|
+
(`sampling/createMessage`, `roots/list`, `elicitation/create`) raise an error.
|
|
1440
|
+
Session-scoped standalone notifications (`resources/updated`, `elicitation/complete`) and
|
|
1441
|
+
broadcast notifications (`tools/list_changed`, etc.) still flow to clients connected to the GET SSE stream.
|
|
1442
|
+
This mode is suitable for simple tool servers that do not need server-initiated requests.
|
|
1443
|
+
|
|
1189
1444
|
By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
|
|
1190
1445
|
When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
|
|
1191
1446
|
|
|
@@ -1194,6 +1449,90 @@ When configured, sessions that receive no HTTP requests for this duration are au
|
|
|
1194
1449
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
|
|
1195
1450
|
```
|
|
1196
1451
|
|
|
1452
|
+
### Pagination
|
|
1453
|
+
|
|
1454
|
+
The MCP Ruby SDK supports [pagination](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination)
|
|
1455
|
+
for list operations that may return large result sets. Pagination uses string cursor tokens carrying a zero-based offset,
|
|
1456
|
+
treated as opaque by clients: the server decides page size, and the client follows `nextCursor` until the server omits it.
|
|
1457
|
+
|
|
1458
|
+
Pagination applies to `tools/list`, `prompts/list`, `resources/list`, and `resources/templates/list`.
|
|
1459
|
+
|
|
1460
|
+
#### Server-Side: Enabling Pagination
|
|
1461
|
+
|
|
1462
|
+
Pass `page_size:` to `MCP::Server.new` to split list responses into pages. When `page_size` is omitted (the default),
|
|
1463
|
+
list responses contain all items in a single response, preserving the pre-pagination behavior.
|
|
1464
|
+
|
|
1465
|
+
```ruby
|
|
1466
|
+
server = MCP::Server.new(
|
|
1467
|
+
name: "my_server",
|
|
1468
|
+
tools: tools,
|
|
1469
|
+
page_size: 50,
|
|
1470
|
+
)
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
When `page_size` is set, list responses include a `nextCursor` field whenever more pages are available:
|
|
1474
|
+
|
|
1475
|
+
```json
|
|
1476
|
+
{
|
|
1477
|
+
"jsonrpc": "2.0",
|
|
1478
|
+
"id": 1,
|
|
1479
|
+
"result": {
|
|
1480
|
+
"tools": [
|
|
1481
|
+
{ "name": "example_tool" }
|
|
1482
|
+
],
|
|
1483
|
+
"nextCursor": "50"
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
Invalid cursors (e.g. non-numeric, negative, or out-of-range) are rejected with JSON-RPC error code `-32602 (Invalid params)` per the MCP specification.
|
|
1489
|
+
|
|
1490
|
+
#### Client-Side: Iterating Pages
|
|
1491
|
+
|
|
1492
|
+
`MCP::Client` exposes `list_tools`, `list_prompts`, `list_resources`, and `list_resource_templates`.
|
|
1493
|
+
**Each call issues exactly one `*/list` JSON-RPC request and returns exactly one page** — not the full collection.
|
|
1494
|
+
The returned result object (`MCP::Client::ListToolsResult` etc.) exposes the page items and the next cursor as method accessors:
|
|
1495
|
+
|
|
1496
|
+
```ruby
|
|
1497
|
+
client = MCP::Client.new(transport: transport)
|
|
1498
|
+
|
|
1499
|
+
cursor = nil
|
|
1500
|
+
loop do
|
|
1501
|
+
page = client.list_tools(cursor: cursor)
|
|
1502
|
+
page.tools.each { |tool| process(tool) }
|
|
1503
|
+
cursor = page.next_cursor
|
|
1504
|
+
break unless cursor
|
|
1505
|
+
end
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
The same pattern applies to `list_prompts` (`page.prompts`), `list_resources` (`page.resources`), and
|
|
1509
|
+
`list_resource_templates` (`page.resource_templates`). `next_cursor` is `nil` on the final page.
|
|
1510
|
+
|
|
1511
|
+
Because a single call returns a single page, how many items come back depends on the server's `page_size` configuration:
|
|
1512
|
+
|
|
1513
|
+
| Server `page_size` | `client.list_tools(cursor: nil)` |
|
|
1514
|
+
|--------------------|---------------------------------------------------------------------|
|
|
1515
|
+
| Not set (default) | Returns every item in one response. `next_cursor` is `nil`. |
|
|
1516
|
+
| Set to `N` | Returns the first `N` items. `next_cursor` is set for continuation. |
|
|
1517
|
+
|
|
1518
|
+
If your application needs the complete collection regardless of how the server is configured, either loop on
|
|
1519
|
+
`next_cursor` as shown above, or use the whole-collection methods described below.
|
|
1520
|
+
|
|
1521
|
+
#### Fetching the Complete Collection
|
|
1522
|
+
|
|
1523
|
+
`client.tools`, `client.resources`, `client.resource_templates`, and `client.prompts` auto-iterate
|
|
1524
|
+
through all pages and return a plain array of items, guaranteeing the full collection regardless
|
|
1525
|
+
of the server's `page_size` setting. When a server paginates, they issue multiple JSON-RPC round
|
|
1526
|
+
trips per call and break out of the pagination loop if the server returns the same `nextCursor`
|
|
1527
|
+
twice in a row as a safety measure.
|
|
1528
|
+
|
|
1529
|
+
```ruby
|
|
1530
|
+
tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
|
|
1534
|
+
fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
|
|
1535
|
+
|
|
1197
1536
|
### Advanced
|
|
1198
1537
|
|
|
1199
1538
|
#### Custom Methods
|
|
@@ -1247,17 +1586,13 @@ end
|
|
|
1247
1586
|
- Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
|
|
1248
1587
|
- Supports the same exception reporting and instrumentation as standard methods
|
|
1249
1588
|
|
|
1250
|
-
### Unsupported Features (to be implemented in future versions)
|
|
1251
|
-
|
|
1252
|
-
- Resource subscriptions
|
|
1253
|
-
- Elicitation
|
|
1254
|
-
|
|
1255
1589
|
## Building an MCP Client
|
|
1256
1590
|
|
|
1257
1591
|
The `MCP::Client` class provides an interface for interacting with MCP servers.
|
|
1258
1592
|
|
|
1259
1593
|
This class supports:
|
|
1260
1594
|
|
|
1595
|
+
- Liveness check via the `ping` method (`MCP::Client#ping`)
|
|
1261
1596
|
- Tool listing via the `tools/list` method (`MCP::Client#tools`)
|
|
1262
1597
|
- Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
|
|
1263
1598
|
- Resource listing via the `resources/list` method (`MCP::Client#resources`)
|
|
@@ -1344,11 +1679,12 @@ The stdio transport automatically handles:
|
|
|
1344
1679
|
|
|
1345
1680
|
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
|
|
1346
1681
|
|
|
1347
|
-
You'll need to add `faraday` as a dependency in order to use the HTTP transport layer:
|
|
1682
|
+
You'll need to add `faraday` as a dependency in order to use the HTTP transport layer. Add `event_stream_parser` as well if the server uses SSE (`text/event-stream`) responses:
|
|
1348
1683
|
|
|
1349
1684
|
```ruby
|
|
1350
1685
|
gem 'mcp'
|
|
1351
1686
|
gem 'faraday', '>= 2.0'
|
|
1687
|
+
gem 'event_stream_parser', '>= 1.0' # optional, required only for SSE responses
|
|
1352
1688
|
```
|
|
1353
1689
|
|
|
1354
1690
|
Example usage:
|