mcp 0.13.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 +373 -6
- data/lib/json_rpc_handler.rb +6 -0
- data/lib/mcp/cancellation.rb +72 -0
- data/lib/mcp/cancelled_error.rb +13 -0
- data/lib/mcp/client/http.rb +230 -14
- data/lib/mcp/client/paginated_result.rb +13 -0
- data/lib/mcp/client/stdio.rb +100 -49
- data/lib/mcp/client.rb +235 -22
- data/lib/mcp/methods.rb +2 -4
- data/lib/mcp/server/pagination.rb +42 -0
- data/lib/mcp/server/transports/stdio_transport.rb +7 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +82 -7
- data/lib/mcp/server.rb +204 -33
- data/lib/mcp/server_context.rb +30 -1
- data/lib/mcp/server_session.rb +121 -20
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- metadata +8 -5
- data/lib/mcp/transports/stdio.rb +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d7b34843d1b915df5d3df424b025c93bba1d50a5e19486f2dcd4a646227a749
|
|
4
|
+
data.tar.gz: 4327576e74518a21d1dbe57f7f22a6f17c8e486b7a9fb9c281c11d206dcd007e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afe69100125bcc47dbfca963983eca401f5bf29b31c01748dd1a5e99537542a4cb2b021573d3635c94bb410cdd430c0e3ff023796d91299da366cc32c7999fba
|
|
7
|
+
data.tar.gz: 815153266ba50a7e3045d7058af383d2ba852e1eefbaa62bc78299da89621bb1c0e22c5d2738f56470f50b4e8bf85f0d83135b1789f2faa9c841cff5e29fa766
|
data/README.md
CHANGED
|
@@ -38,7 +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)
|
|
41
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)
|
|
42
45
|
|
|
43
46
|
### Supported Methods
|
|
44
47
|
|
|
@@ -51,7 +54,10 @@ It implements the Model Context Protocol specification, handling model context r
|
|
|
51
54
|
- `resources/list` - Lists all registered resources and their schemas
|
|
52
55
|
- `resources/read` - Retrieves a specific resource by name
|
|
53
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
|
|
54
59
|
- `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
|
|
60
|
+
- `roots/list` - Requests filesystem roots from the client (server-to-client)
|
|
55
61
|
- `sampling/createMessage` - Requests LLM completion from the client (server-to-client)
|
|
56
62
|
- `elicitation/create` - Requests user input from the client (server-to-client)
|
|
57
63
|
|
|
@@ -891,6 +897,108 @@ server = MCP::Server.new(
|
|
|
891
897
|
)
|
|
892
898
|
```
|
|
893
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:**
|
|
906
|
+
|
|
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
|
|
910
|
+
|
|
911
|
+
**Using Roots in Tools:**
|
|
912
|
+
|
|
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"
|
|
919
|
+
input_schema(
|
|
920
|
+
properties: {
|
|
921
|
+
query: { type: "string" }
|
|
922
|
+
},
|
|
923
|
+
required: ["query"]
|
|
924
|
+
)
|
|
925
|
+
|
|
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
|
+
}])
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
Result contains an array of root objects:
|
|
939
|
+
|
|
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
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
**Handling Root Changes:**
|
|
950
|
+
|
|
951
|
+
Register a callback to be notified when the client's roots change:
|
|
952
|
+
|
|
953
|
+
```ruby
|
|
954
|
+
server.roots_list_changed_handler do
|
|
955
|
+
puts "Client's roots have changed, tools will see updated roots on next call."
|
|
956
|
+
end
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**Error Handling:**
|
|
960
|
+
|
|
961
|
+
- Raises `RuntimeError` if client does not support `roots` capability
|
|
962
|
+
- Raises `StandardError` if client returns an error response
|
|
963
|
+
|
|
964
|
+
### Resource Subscriptions
|
|
965
|
+
|
|
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.
|
|
968
|
+
|
|
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:
|
|
972
|
+
|
|
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
|
|
976
|
+
|
|
977
|
+
```ruby
|
|
978
|
+
subscribed_uris = Set.new
|
|
979
|
+
|
|
980
|
+
server = MCP::Server.new(
|
|
981
|
+
name: "my_server",
|
|
982
|
+
resources: [my_resource],
|
|
983
|
+
capabilities: { resources: { subscribe: true } },
|
|
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
|
|
1000
|
+
```
|
|
1001
|
+
|
|
894
1002
|
### Sampling
|
|
895
1003
|
|
|
896
1004
|
The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.
|
|
@@ -989,9 +1097,164 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
|
|
|
989
1097
|
- `notifications/tools/list_changed`
|
|
990
1098
|
- `notifications/prompts/list_changed`
|
|
991
1099
|
- `notifications/resources/list_changed`
|
|
1100
|
+
- `notifications/cancelled`
|
|
992
1101
|
- `notifications/progress`
|
|
993
1102
|
- `notifications/message`
|
|
994
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.
|
|
1116
|
+
|
|
1117
|
+
> [!NOTE]
|
|
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.
|
|
1123
|
+
|
|
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:
|
|
1132
|
+
|
|
1133
|
+
```ruby
|
|
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
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
Alternatively, raise at the next safe point with `raise_if_cancelled!`:
|
|
1152
|
+
|
|
1153
|
+
```ruby
|
|
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" }])
|
|
1162
|
+
end
|
|
1163
|
+
```
|
|
1164
|
+
|
|
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.
|
|
1168
|
+
|
|
1169
|
+
The same pattern works for other handler types:
|
|
1170
|
+
|
|
1171
|
+
```ruby
|
|
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
|
|
1199
|
+
```
|
|
1200
|
+
|
|
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.
|
|
1203
|
+
|
|
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:
|
|
1211
|
+
|
|
1212
|
+
```ruby
|
|
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
|
|
1223
|
+
```
|
|
1224
|
+
|
|
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.
|
|
1230
|
+
|
|
1231
|
+
### Ping
|
|
1232
|
+
|
|
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.
|
|
1237
|
+
|
|
1238
|
+
#### Server-Side
|
|
1239
|
+
|
|
1240
|
+
Servers respond to incoming `ping` requests automatically - no setup is required.
|
|
1241
|
+
Any `MCP::Server` instance replies with an empty result.
|
|
1242
|
+
|
|
1243
|
+
#### Client-Side
|
|
1244
|
+
|
|
1245
|
+
`MCP::Client` exposes `ping` to send a ping to the server:
|
|
1246
|
+
|
|
1247
|
+
```ruby
|
|
1248
|
+
client = MCP::Client.new(transport: transport)
|
|
1249
|
+
client.ping # => {} on success
|
|
1250
|
+
```
|
|
1251
|
+
|
|
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.
|
|
1257
|
+
|
|
995
1258
|
### Progress
|
|
996
1259
|
|
|
997
1260
|
The MCP Ruby SDK supports progress tracking for long-running tool operations,
|
|
@@ -1291,6 +1554,22 @@ Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`
|
|
|
1291
1554
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
|
|
1292
1555
|
```
|
|
1293
1556
|
|
|
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`:
|
|
1559
|
+
|
|
1560
|
+
```ruby
|
|
1561
|
+
# JSON response mode
|
|
1562
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, enable_json_response: true)
|
|
1563
|
+
```
|
|
1564
|
+
|
|
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
|
+
|
|
1294
1573
|
By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
|
|
1295
1574
|
When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
|
|
1296
1575
|
|
|
@@ -1299,6 +1578,90 @@ When configured, sessions that receive no HTTP requests for this duration are au
|
|
|
1299
1578
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
|
|
1300
1579
|
```
|
|
1301
1580
|
|
|
1581
|
+
### Pagination
|
|
1582
|
+
|
|
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.
|
|
1586
|
+
|
|
1587
|
+
Pagination applies to `tools/list`, `prompts/list`, `resources/list`, and `resources/templates/list`.
|
|
1588
|
+
|
|
1589
|
+
#### Server-Side: Enabling Pagination
|
|
1590
|
+
|
|
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.
|
|
1593
|
+
|
|
1594
|
+
```ruby
|
|
1595
|
+
server = MCP::Server.new(
|
|
1596
|
+
name: "my_server",
|
|
1597
|
+
tools: tools,
|
|
1598
|
+
page_size: 50,
|
|
1599
|
+
)
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
When `page_size` is set, list responses include a `nextCursor` field whenever more pages are available:
|
|
1603
|
+
|
|
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
|
+
```
|
|
1616
|
+
|
|
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.
|
|
1618
|
+
|
|
1619
|
+
#### Client-Side: Iterating Pages
|
|
1620
|
+
|
|
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:
|
|
1624
|
+
|
|
1625
|
+
```ruby
|
|
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
|
+
```
|
|
1636
|
+
|
|
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.
|
|
1660
|
+
```
|
|
1661
|
+
|
|
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
|
+
|
|
1302
1665
|
### Advanced
|
|
1303
1666
|
|
|
1304
1667
|
#### Custom Methods
|
|
@@ -1352,21 +1715,18 @@ end
|
|
|
1352
1715
|
- Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
|
|
1353
1716
|
- Supports the same exception reporting and instrumentation as standard methods
|
|
1354
1717
|
|
|
1355
|
-
### Unsupported Features (to be implemented in future versions)
|
|
1356
|
-
|
|
1357
|
-
- Resource subscriptions
|
|
1358
|
-
|
|
1359
1718
|
## Building an MCP Client
|
|
1360
1719
|
|
|
1361
1720
|
The `MCP::Client` class provides an interface for interacting with MCP servers.
|
|
1362
1721
|
|
|
1363
1722
|
This class supports:
|
|
1364
1723
|
|
|
1724
|
+
- Liveness check via the `ping` method (`MCP::Client#ping`)
|
|
1365
1725
|
- Tool listing via the `tools/list` method (`MCP::Client#tools`)
|
|
1366
1726
|
- Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
|
|
1367
1727
|
- Resource listing via the `resources/list` method (`MCP::Client#resources`)
|
|
1368
1728
|
- Resource template listing via the `resources/templates/list` method (`MCP::Client#resource_templates`)
|
|
1369
|
-
- Resource reading via the `resources/read` method (`MCP::Client#
|
|
1729
|
+
- Resource reading via the `resources/read` method (`MCP::Client#read_resource`)
|
|
1370
1730
|
- Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
|
|
1371
1731
|
- Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
|
|
1372
1732
|
- Completion requests via the `completion/complete` method (`MCP::Client#complete`)
|
|
@@ -1422,6 +1782,9 @@ stdio_transport = MCP::Client::Stdio.new(
|
|
|
1422
1782
|
)
|
|
1423
1783
|
client = MCP::Client.new(transport: stdio_transport)
|
|
1424
1784
|
|
|
1785
|
+
# Perform the MCP initialization handshake before sending any requests.
|
|
1786
|
+
client.connect
|
|
1787
|
+
|
|
1425
1788
|
# List available tools.
|
|
1426
1789
|
tools = client.tools
|
|
1427
1790
|
tools.each do |tool|
|
|
@@ -1448,11 +1811,12 @@ The stdio transport automatically handles:
|
|
|
1448
1811
|
|
|
1449
1812
|
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
|
|
1450
1813
|
|
|
1451
|
-
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:
|
|
1452
1815
|
|
|
1453
1816
|
```ruby
|
|
1454
1817
|
gem 'mcp'
|
|
1455
1818
|
gem 'faraday', '>= 2.0'
|
|
1819
|
+
gem 'event_stream_parser', '>= 1.0' # optional, required only for SSE responses
|
|
1456
1820
|
```
|
|
1457
1821
|
|
|
1458
1822
|
Example usage:
|
|
@@ -1461,6 +1825,9 @@ Example usage:
|
|
|
1461
1825
|
http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
|
|
1462
1826
|
client = MCP::Client.new(transport: http_transport)
|
|
1463
1827
|
|
|
1828
|
+
# Perform the MCP initialization handshake before sending any requests.
|
|
1829
|
+
client.connect
|
|
1830
|
+
|
|
1464
1831
|
# List available tools
|
|
1465
1832
|
tools = client.tools
|
|
1466
1833
|
tools.each do |tool|
|
data/lib/json_rpc_handler.rb
CHANGED
|
@@ -18,6 +18,11 @@ module JsonRpcHandler
|
|
|
18
18
|
|
|
19
19
|
DEFAULT_ALLOWED_ID_CHARACTERS = /\A[a-zA-Z0-9_-]+\z/
|
|
20
20
|
|
|
21
|
+
# Sentinel return value from a handler. When a handler returns this,
|
|
22
|
+
# `process_request` emits no JSON-RPC response for the request,
|
|
23
|
+
# matching the notification-style semantics (id is ignored).
|
|
24
|
+
NO_RESPONSE = Object.new.freeze
|
|
25
|
+
|
|
21
26
|
extend self
|
|
22
27
|
|
|
23
28
|
def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
|
|
@@ -103,6 +108,7 @@ module JsonRpcHandler
|
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
result = method.call(params)
|
|
111
|
+
return if result.equal?(NO_RESPONSE)
|
|
106
112
|
|
|
107
113
|
success_response(id: id, result: result)
|
|
108
114
|
rescue MCP::Server::RequestHandlerError => e
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cancelled_error"
|
|
4
|
+
|
|
5
|
+
module MCP
|
|
6
|
+
class Cancellation
|
|
7
|
+
attr_reader :reason, :request_id
|
|
8
|
+
|
|
9
|
+
def initialize(request_id: nil)
|
|
10
|
+
@request_id = request_id
|
|
11
|
+
@reason = nil
|
|
12
|
+
@cancelled = false
|
|
13
|
+
@callbacks = []
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cancelled?
|
|
18
|
+
@mutex.synchronize { @cancelled }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cancel(reason: nil)
|
|
22
|
+
callbacks = @mutex.synchronize do
|
|
23
|
+
return false if @cancelled
|
|
24
|
+
|
|
25
|
+
@cancelled = true
|
|
26
|
+
@reason = reason
|
|
27
|
+
@callbacks.tap { @callbacks = [] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
callbacks.each do |callback|
|
|
31
|
+
callback.call(reason)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
MCP.configuration.exception_reporter.call(e, { error: "Cancellation callback failed" })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Registers a callback invoked synchronously on the first `cancel` call.
|
|
40
|
+
# If already cancelled, fires immediately.
|
|
41
|
+
#
|
|
42
|
+
# Returns the block itself as a handle that can be passed to `off_cancel`
|
|
43
|
+
# to deregister it (e.g. when a nested request completes normally and the
|
|
44
|
+
# hook should not fire on a later parent cancellation).
|
|
45
|
+
def on_cancel(&block)
|
|
46
|
+
fire_now = false
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
if @cancelled
|
|
49
|
+
fire_now = true
|
|
50
|
+
else
|
|
51
|
+
@callbacks << block
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
block.call(@reason) if fire_now
|
|
56
|
+
block
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Removes a previously-registered `on_cancel` callback. Returns `true`
|
|
60
|
+
# if the callback was still pending (i.e. had not yet fired), `false`
|
|
61
|
+
# otherwise. Safe to call with `nil`.
|
|
62
|
+
def off_cancel(handle)
|
|
63
|
+
return false unless handle
|
|
64
|
+
|
|
65
|
+
@mutex.synchronize { !@callbacks.delete(handle).nil? }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def raise_if_cancelled!
|
|
69
|
+
raise CancelledError.new(request_id: @request_id, reason: @reason) if cancelled?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class CancelledError < StandardError
|
|
5
|
+
attr_reader :request_id, :reason
|
|
6
|
+
|
|
7
|
+
def initialize(message = "Request was cancelled", request_id: nil, reason: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@request_id = request_id
|
|
10
|
+
@reason = reason
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|