mcp 0.10.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/mcp/client.rb CHANGED
@@ -2,10 +2,47 @@
2
2
 
3
3
  require_relative "client/stdio"
4
4
  require_relative "client/http"
5
+ require_relative "client/paginated_result"
5
6
  require_relative "client/tool"
6
7
 
7
8
  module MCP
8
9
  class Client
10
+ class ServerError < StandardError
11
+ attr_reader :code, :data
12
+
13
+ def initialize(message, code:, data: nil)
14
+ super(message)
15
+ @code = code
16
+ @data = data
17
+ end
18
+ end
19
+
20
+ class RequestHandlerError < StandardError
21
+ attr_reader :error_type, :original_error, :request
22
+
23
+ def initialize(message, request, error_type: :internal_error, original_error: nil)
24
+ super(message)
25
+ @request = request
26
+ @error_type = error_type
27
+ @original_error = original_error
28
+ end
29
+ end
30
+
31
+ # Raised when a server response fails client-side validation, e.g., a success response
32
+ # whose `result` field is missing or has the wrong type. This is distinct from a
33
+ # server-returned JSON-RPC error, which is raised as `ServerError`.
34
+ class ValidationError < StandardError; end
35
+
36
+ # Raised when the server responds 404 to a request containing a session ID,
37
+ # indicating the session has expired. Inherits from `RequestHandlerError` for
38
+ # backward compatibility with callers that rescue the generic error. Per spec,
39
+ # clients MUST start a new session with a fresh `initialize` request in response.
40
+ class SessionExpiredError < RequestHandlerError
41
+ def initialize(message, request, original_error: nil)
42
+ super(message, request, error_type: :not_found, original_error: original_error)
43
+ end
44
+ end
45
+
9
46
  # Initializes a new MCP::Client instance.
10
47
  #
11
48
  # @param transport [Object] The transport object to use for communication with the server.
@@ -22,8 +59,81 @@ module MCP
22
59
  # So keeping it public
23
60
  attr_reader :transport
24
61
 
25
- # Returns the list of tools available from the server.
26
- # Each call will make a new request the result is not cached.
62
+ # The server's `InitializeResult` (protocol version, capabilities, server info,
63
+ # instructions), as reported by the transport after a successful `connect`.
64
+ # Returns `nil` before `connect`, after `close`, or when the transport does
65
+ # not expose a cached handshake result.
66
+ def server_info
67
+ transport.server_info if transport.respond_to?(:server_info)
68
+ end
69
+
70
+ # Performs the MCP `initialize` handshake by delegating to the transport
71
+ # (e.g. `MCP::Client::HTTP`, `MCP::Client::Stdio`). Returns the server's
72
+ # `InitializeResult`.
73
+ #
74
+ # When the transport does not respond to `:connect`, this is a no-op and
75
+ # returns `nil`.
76
+ #
77
+ # @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
78
+ # @param protocol_version [String, nil] Protocol version to offer.
79
+ # @param capabilities [Hash] Capabilities advertised by the client.
80
+ # @return [Hash, nil] The server's `InitializeResult`, or `nil` when the transport
81
+ # does not expose an explicit handshake.
82
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
83
+ def connect(client_info: nil, protocol_version: nil, capabilities: {})
84
+ return unless transport.respond_to?(:connect)
85
+
86
+ transport.connect(
87
+ client_info: client_info,
88
+ protocol_version: protocol_version,
89
+ capabilities: capabilities,
90
+ )
91
+ end
92
+
93
+ # Returns true once `connect` has completed the handshake on the underlying
94
+ # transport. Transports that do not expose connection state are assumed
95
+ # connected and return `true`.
96
+ def connected?
97
+ return transport.connected? if transport.respond_to?(:connected?)
98
+
99
+ true
100
+ end
101
+
102
+ # Returns a single page of tools from the server.
103
+ #
104
+ # @param cursor [String, nil] Cursor from a previous page response.
105
+ # @return [MCP::Client::ListToolsResult] Result with `tools` (Array<MCP::Client::Tool>)
106
+ # and `next_cursor` (String or nil).
107
+ #
108
+ # @example Iterate all pages
109
+ # cursor = nil
110
+ # loop do
111
+ # page = client.list_tools(cursor: cursor)
112
+ # page.tools.each { |tool| puts tool.name }
113
+ # cursor = page.next_cursor
114
+ # break unless cursor
115
+ # end
116
+ def list_tools(cursor: nil)
117
+ params = cursor ? { cursor: cursor } : nil
118
+ response = request(method: "tools/list", params: params)
119
+ result = response["result"] || {}
120
+
121
+ tools = (result["tools"] || []).map do |tool|
122
+ Tool.new(
123
+ name: tool["name"],
124
+ description: tool["description"],
125
+ input_schema: tool["inputSchema"],
126
+ )
127
+ end
128
+
129
+ ListToolsResult.new(tools: tools, next_cursor: result["nextCursor"], meta: result["_meta"])
130
+ end
131
+
132
+ # Returns every tool available on the server. Iterates through all pages automatically
133
+ # when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
134
+ # Use {#list_tools} when you need fine-grained cursor control.
135
+ #
136
+ # Each call will make a new request - the result is not cached.
27
137
  #
28
138
  # @return [Array<MCP::Client::Tool>] An array of available tools.
29
139
  #
@@ -33,61 +143,151 @@ module MCP
33
143
  # puts tool.name
34
144
  # end
35
145
  def tools
36
- response = transport.send_request(request: {
37
- jsonrpc: JsonRpcHandler::Version::V2_0,
38
- id: request_id,
39
- method: "tools/list",
40
- })
146
+ # TODO: consider renaming to `list_all_tools`.
147
+ all_tools = []
148
+ seen = Set.new
149
+ cursor = nil
41
150
 
42
- response.dig("result", "tools")&.map do |tool|
43
- Tool.new(
44
- name: tool["name"],
45
- description: tool["description"],
46
- input_schema: tool["inputSchema"],
47
- )
48
- end || []
151
+ loop do
152
+ page = list_tools(cursor: cursor)
153
+ all_tools.concat(page.tools)
154
+ next_cursor = page.next_cursor
155
+ break if next_cursor.nil? || seen.include?(next_cursor)
156
+
157
+ seen << next_cursor
158
+ cursor = next_cursor
159
+ end
160
+
161
+ all_tools
49
162
  end
50
163
 
51
- # Returns the list of resources available from the server.
52
- # Each call will make a new request – the result is not cached.
164
+ # Returns a single page of resources from the server.
165
+ #
166
+ # @param cursor [String, nil] Cursor from a previous page response.
167
+ # @return [MCP::Client::ListResourcesResult] Result with `resources` (Array<Hash>)
168
+ # and `next_cursor` (String or nil).
169
+ def list_resources(cursor: nil)
170
+ params = cursor ? { cursor: cursor } : nil
171
+ response = request(method: "resources/list", params: params)
172
+ result = response["result"] || {}
173
+
174
+ ListResourcesResult.new(
175
+ resources: result["resources"] || [],
176
+ next_cursor: result["nextCursor"],
177
+ meta: result["_meta"],
178
+ )
179
+ end
180
+
181
+ # Returns every resource available on the server. Iterates through all pages automatically
182
+ # when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
183
+ # Use {#list_resources} when you need fine-grained cursor control.
184
+ #
185
+ # Each call will make a new request - the result is not cached.
53
186
  #
54
187
  # @return [Array<Hash>] An array of available resources.
55
188
  def resources
56
- response = transport.send_request(request: {
57
- jsonrpc: JsonRpcHandler::Version::V2_0,
58
- id: request_id,
59
- method: "resources/list",
60
- })
189
+ # TODO: consider renaming to `list_all_resources`.
190
+ all_resources = []
191
+ seen = Set.new
192
+ cursor = nil
193
+
194
+ loop do
195
+ page = list_resources(cursor: cursor)
196
+ all_resources.concat(page.resources)
197
+ next_cursor = page.next_cursor
198
+ break if next_cursor.nil? || seen.include?(next_cursor)
199
+
200
+ seen << next_cursor
201
+ cursor = next_cursor
202
+ end
203
+
204
+ all_resources
205
+ end
206
+
207
+ # Returns a single page of resource templates from the server.
208
+ #
209
+ # @param cursor [String, nil] Cursor from a previous page response.
210
+ # @return [MCP::Client::ListResourceTemplatesResult] Result with `resource_templates`
211
+ # (Array<Hash>) and `next_cursor` (String or nil).
212
+ def list_resource_templates(cursor: nil)
213
+ params = cursor ? { cursor: cursor } : nil
214
+ response = request(method: "resources/templates/list", params: params)
215
+ result = response["result"] || {}
61
216
 
62
- response.dig("result", "resources") || []
217
+ ListResourceTemplatesResult.new(
218
+ resource_templates: result["resourceTemplates"] || [],
219
+ next_cursor: result["nextCursor"],
220
+ meta: result["_meta"],
221
+ )
63
222
  end
64
223
 
65
- # Returns the list of resource templates available from the server.
66
- # Each call will make a new request the result is not cached.
224
+ # Returns every resource template available on the server. Iterates through all pages automatically
225
+ # when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
226
+ # Use {#list_resource_templates} when you need fine-grained cursor control.
227
+ #
228
+ # Each call will make a new request - the result is not cached.
67
229
  #
68
230
  # @return [Array<Hash>] An array of available resource templates.
69
231
  def resource_templates
70
- response = transport.send_request(request: {
71
- jsonrpc: JsonRpcHandler::Version::V2_0,
72
- id: request_id,
73
- method: "resources/templates/list",
74
- })
232
+ # TODO: consider renaming to `list_all_resource_templates`.
233
+ all_templates = []
234
+ seen = Set.new
235
+ cursor = nil
236
+
237
+ loop do
238
+ page = list_resource_templates(cursor: cursor)
239
+ all_templates.concat(page.resource_templates)
240
+ next_cursor = page.next_cursor
241
+ break if next_cursor.nil? || seen.include?(next_cursor)
242
+
243
+ seen << next_cursor
244
+ cursor = next_cursor
245
+ end
246
+
247
+ all_templates
248
+ end
249
+
250
+ # Returns a single page of prompts from the server.
251
+ #
252
+ # @param cursor [String, nil] Cursor from a previous page response.
253
+ # @return [MCP::Client::ListPromptsResult] Result with `prompts` (Array<Hash>)
254
+ # and `next_cursor` (String or nil).
255
+ def list_prompts(cursor: nil)
256
+ params = cursor ? { cursor: cursor } : nil
257
+ response = request(method: "prompts/list", params: params)
258
+ result = response["result"] || {}
75
259
 
76
- response.dig("result", "resourceTemplates") || []
260
+ ListPromptsResult.new(
261
+ prompts: result["prompts"] || [],
262
+ next_cursor: result["nextCursor"],
263
+ meta: result["_meta"],
264
+ )
77
265
  end
78
266
 
79
- # Returns the list of prompts available from the server.
80
- # Each call will make a new request the result is not cached.
267
+ # Returns every prompt available on the server. Iterates through all pages automatically
268
+ # when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
269
+ # Use {#list_prompts} when you need fine-grained cursor control.
270
+ #
271
+ # Each call will make a new request - the result is not cached.
81
272
  #
82
273
  # @return [Array<Hash>] An array of available prompts.
83
274
  def prompts
84
- response = transport.send_request(request: {
85
- jsonrpc: JsonRpcHandler::Version::V2_0,
86
- id: request_id,
87
- method: "prompts/list",
88
- })
275
+ # TODO: consider renaming to `list_all_prompts`.
276
+ all_prompts = []
277
+ seen = Set.new
278
+ cursor = nil
89
279
 
90
- response.dig("result", "prompts") || []
280
+ loop do
281
+ page = list_prompts(cursor: cursor)
282
+ all_prompts.concat(page.prompts)
283
+ next_cursor = page.next_cursor
284
+ break if next_cursor.nil? || seen.include?(next_cursor)
285
+
286
+ seen << next_cursor
287
+ cursor = next_cursor
288
+ end
289
+
290
+ all_prompts
91
291
  end
92
292
 
93
293
  # Calls a tool via the transport layer and returns the full response from the server.
@@ -119,12 +319,7 @@ module MCP
119
319
  params[:_meta] = { progressToken: progress_token }
120
320
  end
121
321
 
122
- transport.send_request(request: {
123
- jsonrpc: JsonRpcHandler::Version::V2_0,
124
- id: request_id,
125
- method: "tools/call",
126
- params: params,
127
- })
322
+ request(method: "tools/call", params: params)
128
323
  end
129
324
 
130
325
  # Reads a resource from the server by URI and returns the contents.
@@ -132,12 +327,7 @@ module MCP
132
327
  # @param uri [String] The URI of the resource to read.
133
328
  # @return [Array<Hash>] An array of resource contents (text or blob).
134
329
  def read_resource(uri:)
135
- response = transport.send_request(request: {
136
- jsonrpc: JsonRpcHandler::Version::V2_0,
137
- id: request_id,
138
- method: "resources/read",
139
- params: { uri: uri },
140
- })
330
+ response = request(method: "resources/read", params: { uri: uri })
141
331
 
142
332
  response.dig("result", "contents") || []
143
333
  end
@@ -147,31 +337,68 @@ module MCP
147
337
  # @param name [String] The name of the prompt to get.
148
338
  # @return [Hash] A hash containing the prompt details.
149
339
  def get_prompt(name:)
150
- response = transport.send_request(request: {
151
- jsonrpc: JsonRpcHandler::Version::V2_0,
152
- id: request_id,
153
- method: "prompts/get",
154
- params: { name: name },
155
- })
340
+ response = request(method: "prompts/get", params: { name: name })
156
341
 
157
342
  response.fetch("result", {})
158
343
  end
159
344
 
160
- private
345
+ # Requests completion suggestions from the server for a prompt argument or resource template URI.
346
+ #
347
+ # @param ref [Hash] The reference, e.g. `{ type: "ref/prompt", name: "my_prompt" }`
348
+ # or `{ type: "ref/resource", uri: "file:///{path}" }`.
349
+ # @param argument [Hash] The argument being completed, e.g. `{ name: "language", value: "py" }`.
350
+ # @param context [Hash, nil] Optional context with previously resolved arguments.
351
+ # @return [Hash] The completion result with `"values"`, `"hasMore"`, and optionally `"total"`.
352
+ def complete(ref:, argument:, context: nil)
353
+ params = { ref: ref, argument: argument }
354
+ params[:context] = context if context
161
355
 
162
- def request_id
163
- SecureRandom.uuid
356
+ response = request(method: "completion/complete", params: params)
357
+
358
+ response.dig("result", "completion") || { "values" => [], "hasMore" => false }
164
359
  end
165
360
 
166
- class RequestHandlerError < StandardError
167
- attr_reader :error_type, :original_error, :request
361
+ # Sends a `ping` request to the server to verify the connection is alive.
362
+ # Per the MCP spec, the server responds with an empty result.
363
+ #
364
+ # @return [Hash] An empty hash on success.
365
+ # @raise [ServerError] If the server returns a JSON-RPC error.
366
+ # @raise [ValidationError] If the response `result` is missing or not a Hash.
367
+ #
368
+ # @example
369
+ # client.ping # => {}
370
+ #
371
+ # @see https://modelcontextprotocol.io/specification/latest/basic/utilities/ping
372
+ def ping
373
+ result = request(method: Methods::PING)["result"]
374
+ raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash)
168
375
 
169
- def initialize(message, request, error_type: :internal_error, original_error: nil)
170
- super(message)
171
- @request = request
172
- @error_type = error_type
173
- @original_error = original_error
376
+ result
377
+ end
378
+
379
+ private
380
+
381
+ def request(method:, params: nil)
382
+ request_body = {
383
+ jsonrpc: JsonRpcHandler::Version::V2_0,
384
+ id: request_id,
385
+ method: method,
386
+ }
387
+ request_body[:params] = params if params
388
+
389
+ response = transport.send_request(request: request_body)
390
+
391
+ # Guard with `is_a?(Hash)` because custom transports may return non-Hash values.
392
+ if response.is_a?(Hash) && response.key?("error")
393
+ error = response["error"]
394
+ raise ServerError.new(error["message"], code: error["code"], data: error["data"])
174
395
  end
396
+
397
+ response
398
+ end
399
+
400
+ def request_id
401
+ SecureRandom.uuid
175
402
  end
176
403
  end
177
404
  end
@@ -7,11 +7,18 @@ module MCP
7
7
  LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
8
8
  ]
9
9
 
10
- attr_writer :exception_reporter, :instrumentation_callback
10
+ attr_writer :exception_reporter, :around_request
11
11
 
12
- def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil,
12
+ # @deprecated Use {#around_request=} instead. `instrumentation_callback`
13
+ # fires only after a request completes and cannot wrap execution in a
14
+ # surrounding block (e.g. for Application Performance Monitoring (APM) spans).
15
+ # @see #around_request=
16
+ attr_writer :instrumentation_callback
17
+
18
+ def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
13
19
  validate_tool_call_arguments: true)
14
20
  @exception_reporter = exception_reporter
21
+ @around_request = around_request
15
22
  @instrumentation_callback = instrumentation_callback
16
23
  @protocol_version = protocol_version
17
24
  if protocol_version
@@ -50,10 +57,24 @@ module MCP
50
57
  !@exception_reporter.nil?
51
58
  end
52
59
 
60
+ def around_request
61
+ @around_request || default_around_request
62
+ end
63
+
64
+ def around_request?
65
+ !@around_request.nil?
66
+ end
67
+
68
+ # @deprecated Use {#around_request} instead. `instrumentation_callback`
69
+ # fires only after a request completes and cannot wrap execution in a
70
+ # surrounding block (e.g. for Application Performance Monitoring (APM) spans).
71
+ # @see #around_request
53
72
  def instrumentation_callback
54
73
  @instrumentation_callback || default_instrumentation_callback
55
74
  end
56
75
 
76
+ # @deprecated Use {#around_request?} instead.
77
+ # @see #around_request?
57
78
  def instrumentation_callback?
58
79
  !@instrumentation_callback.nil?
59
80
  end
@@ -72,20 +93,30 @@ module MCP
72
93
  else
73
94
  @exception_reporter
74
95
  end
96
+
97
+ around_request = if other.around_request?
98
+ other.around_request
99
+ else
100
+ @around_request
101
+ end
102
+
75
103
  instrumentation_callback = if other.instrumentation_callback?
76
104
  other.instrumentation_callback
77
105
  else
78
106
  @instrumentation_callback
79
107
  end
108
+
80
109
  protocol_version = if other.protocol_version?
81
110
  other.protocol_version
82
111
  else
83
112
  @protocol_version
84
113
  end
114
+
85
115
  validate_tool_call_arguments = other.validate_tool_call_arguments
86
116
 
87
117
  Configuration.new(
88
118
  exception_reporter: exception_reporter,
119
+ around_request: around_request,
89
120
  instrumentation_callback: instrumentation_callback,
90
121
  protocol_version: protocol_version,
91
122
  validate_tool_call_arguments: validate_tool_call_arguments,
@@ -111,6 +142,11 @@ module MCP
111
142
  @default_exception_reporter ||= ->(exception, server_context) {}
112
143
  end
113
144
 
145
+ def default_around_request
146
+ @default_around_request ||= ->(_data, &request_handler) { request_handler.call }
147
+ end
148
+
149
+ # @deprecated Use {#default_around_request} instead.
114
150
  def default_instrumentation_callback
115
151
  @default_instrumentation_callback ||= ->(data) {}
116
152
  end
data/lib/mcp/content.rb CHANGED
@@ -3,56 +3,60 @@
3
3
  module MCP
4
4
  module Content
5
5
  class Text
6
- attr_reader :text, :annotations
6
+ attr_reader :text, :annotations, :meta
7
7
 
8
- def initialize(text, annotations: nil)
8
+ def initialize(text, annotations: nil, meta: nil)
9
9
  @text = text
10
10
  @annotations = annotations
11
+ @meta = meta
11
12
  end
12
13
 
13
14
  def to_h
14
- { text: text, annotations: annotations, type: "text" }.compact
15
+ { text: text, annotations: annotations, _meta: meta, type: "text" }.compact
15
16
  end
16
17
  end
17
18
 
18
19
  class Image
19
- attr_reader :data, :mime_type, :annotations
20
+ attr_reader :data, :mime_type, :annotations, :meta
20
21
 
21
- def initialize(data, mime_type, annotations: nil)
22
+ def initialize(data, mime_type, annotations: nil, meta: nil)
22
23
  @data = data
23
24
  @mime_type = mime_type
24
25
  @annotations = annotations
26
+ @meta = meta
25
27
  end
26
28
 
27
29
  def to_h
28
- { data: data, mimeType: mime_type, annotations: annotations, type: "image" }.compact
30
+ { data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "image" }.compact
29
31
  end
30
32
  end
31
33
 
32
34
  class Audio
33
- attr_reader :data, :mime_type, :annotations
35
+ attr_reader :data, :mime_type, :annotations, :meta
34
36
 
35
- def initialize(data, mime_type, annotations: nil)
37
+ def initialize(data, mime_type, annotations: nil, meta: nil)
36
38
  @data = data
37
39
  @mime_type = mime_type
38
40
  @annotations = annotations
41
+ @meta = meta
39
42
  end
40
43
 
41
44
  def to_h
42
- { data: data, mimeType: mime_type, annotations: annotations, type: "audio" }.compact
45
+ { data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "audio" }.compact
43
46
  end
44
47
  end
45
48
 
46
49
  class EmbeddedResource
47
- attr_reader :resource, :annotations
50
+ attr_reader :resource, :annotations, :meta
48
51
 
49
- def initialize(resource, annotations: nil)
52
+ def initialize(resource, annotations: nil, meta: nil)
50
53
  @resource = resource
51
54
  @annotations = annotations
55
+ @meta = meta
52
56
  end
53
57
 
54
58
  def to_h
55
- { resource: resource.to_h, annotations: annotations, type: "resource" }.compact
59
+ { resource: resource.to_h, annotations: annotations, _meta: meta, type: "resource" }.compact
56
60
  end
57
61
  end
58
62
  end
@@ -2,19 +2,40 @@
2
2
 
3
3
  module MCP
4
4
  module Instrumentation
5
- def instrument_call(method, &block)
5
+ def instrument_call(method, server_context: {}, exception_already_reported: nil, &block)
6
6
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
7
  begin
8
8
  @instrumentation_data = {}
9
9
  add_instrumentation_data(method: method)
10
10
 
11
- result = yield block
11
+ result = configuration.around_request.call(@instrumentation_data, &block)
12
12
 
13
13
  result
14
+ rescue => e
15
+ already_reported = begin
16
+ !!exception_already_reported&.call(e)
17
+ # rubocop:disable Lint/RescueException
18
+ rescue Exception
19
+ # rubocop:enable Lint/RescueException
20
+ # The predicate is expected to be side-effect-free and return a boolean.
21
+ # Any exception raised from it (including non-StandardError such as SystemExit)
22
+ # must not shadow the original exception.
23
+ false
24
+ end
25
+
26
+ unless already_reported
27
+ add_instrumentation_data(error: :internal_error) unless @instrumentation_data.key?(:error)
28
+ configuration.exception_reporter.call(e, server_context)
29
+ end
30
+
31
+ raise
14
32
  ensure
15
33
  end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
34
  add_instrumentation_data(duration: end_time - start_time)
17
35
 
36
+ # Backward compatibility: `instrumentation_callback` is soft-deprecated
37
+ # in favor of `around_request`, but existing callers still expect it
38
+ # to fire after every request until it is removed in a future version.
18
39
  configuration.instrumentation_callback.call(@instrumentation_data)
19
40
  end
20
41
  end
data/lib/mcp/methods.rb CHANGED
@@ -33,6 +33,7 @@ module MCP
33
33
  NOTIFICATIONS_MESSAGE = "notifications/message"
34
34
  NOTIFICATIONS_PROGRESS = "notifications/progress"
35
35
  NOTIFICATIONS_CANCELLED = "notifications/cancelled"
36
+ NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
36
37
 
37
38
  class MissingRequiredCapabilityError < StandardError
38
39
  attr_reader :method
@@ -72,15 +73,13 @@ module MCP
72
73
  require_capability!(method, capabilities, :completions)
73
74
  when ROOTS_LIST
74
75
  require_capability!(method, capabilities, :roots)
75
- when NOTIFICATIONS_ROOTS_LIST_CHANGED
76
- require_capability!(method, capabilities, :roots)
77
- require_capability!(method, capabilities, :roots, :listChanged)
78
76
  when SAMPLING_CREATE_MESSAGE
79
77
  require_capability!(method, capabilities, :sampling)
80
78
  when ELICITATION_CREATE
81
79
  require_capability!(method, capabilities, :elicitation)
82
- when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED
83
- # No specific capability required for initialize, ping, progress or cancelled
80
+ when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_ROOTS_LIST_CHANGED,
81
+ NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED, NOTIFICATIONS_ELICITATION_COMPLETE
82
+ # No specific capability required.
84
83
  end
85
84
  end
86
85
 
data/lib/mcp/progress.rb CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  module MCP
4
4
  class Progress
5
- def initialize(notification_target:, progress_token:)
5
+ def initialize(notification_target:, progress_token:, related_request_id: nil)
6
6
  @notification_target = notification_target
7
7
  @progress_token = progress_token
8
+ @related_request_id = related_request_id
8
9
  end
9
10
 
10
11
  def report(progress, total: nil, message: nil)
@@ -16,6 +17,7 @@ module MCP
16
17
  progress: progress,
17
18
  total: total,
18
19
  message: message,
20
+ related_request_id: @related_request_id,
19
21
  )
20
22
  end
21
23
  end