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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6a956181733036c09b8431db1d1da47e3757165123efd5d5dab5fe1cc522e6a
4
- data.tar.gz: fc611e55e4f88e9ca61ad64d4d2578c6e5a9680ceb4d667337787098ca59bbde
3
+ metadata.gz: 0d7b34843d1b915df5d3df424b025c93bba1d50a5e19486f2dcd4a646227a749
4
+ data.tar.gz: 4327576e74518a21d1dbe57f7f22a6f17c8e486b7a9fb9c281c11d206dcd007e
5
5
  SHA512:
6
- metadata.gz: '010991cbd4a1b18214d12d8ce3cb09b3658cba4a1cf72c7475ff76688fe87eef4137bd76eb5ebd7b15309e1aec7ed84909125858accd474f052e1a915ec37ad8'
7
- data.tar.gz: 5b9b2994a4fb3769bda007a39ce0cf02c2a9b4fbeeecb48dc74aa4ca66e4d6e066c545f5d473b339561af06cf14f8e546a6e87a92d4d9c0939fe90858cddb3a8
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#read_resources`)
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|
@@ -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