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.
@@ -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
 
@@ -3,15 +3,16 @@
3
3
  module MCP
4
4
  class Prompt
5
5
  class Result
6
- attr_reader :description, :messages
6
+ attr_reader :description, :messages, :meta
7
7
 
8
- def initialize(description: nil, messages: [])
8
+ def initialize(description: nil, messages: [], meta: nil)
9
9
  @description = description
10
10
  @messages = messages
11
+ @meta = meta
11
12
  end
12
13
 
13
14
  def to_h
14
- { description: description, messages: messages.map(&:to_h) }.compact
15
+ { description: description, messages: messages.map(&:to_h), _meta: meta }.compact
15
16
  end
16
17
  end
17
18
  end
@@ -3,23 +3,24 @@
3
3
  module MCP
4
4
  class Resource
5
5
  class Contents
6
- attr_reader :uri, :mime_type
6
+ attr_reader :uri, :mime_type, :meta
7
7
 
8
- def initialize(uri:, mime_type: nil)
8
+ def initialize(uri:, mime_type: nil, meta: nil)
9
9
  @uri = uri
10
10
  @mime_type = mime_type
11
+ @meta = meta
11
12
  end
12
13
 
13
14
  def to_h
14
- { uri: uri, mimeType: mime_type }.compact
15
+ { uri: uri, mimeType: mime_type, _meta: meta }.compact
15
16
  end
16
17
  end
17
18
 
18
19
  class TextContents < Contents
19
20
  attr_reader :text
20
21
 
21
- def initialize(text:, uri:, mime_type:)
22
- super(uri: uri, mime_type: mime_type)
22
+ def initialize(text:, uri:, mime_type:, meta: nil)
23
+ super(uri: uri, mime_type: mime_type, meta: meta)
23
24
  @text = text
24
25
  end
25
26
 
@@ -31,8 +32,8 @@ module MCP
31
32
  class BlobContents < Contents
32
33
  attr_reader :data
33
34
 
34
- def initialize(data:, uri:, mime_type:)
35
- super(uri: uri, mime_type: mime_type)
35
+ def initialize(data:, uri:, mime_type:, meta: nil)
36
+ super(uri: uri, mime_type: mime_type, meta: meta)
36
37
  @data = data
37
38
  end
38
39
 
data/lib/mcp/resource.rb CHANGED
@@ -5,15 +5,16 @@ require_relative "resource/embedded"
5
5
 
6
6
  module MCP
7
7
  class Resource
8
- attr_reader :uri, :name, :title, :description, :icons, :mime_type
8
+ attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta
9
9
 
10
- def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
10
+ def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
11
11
  @uri = uri
12
12
  @name = name
13
13
  @title = title
14
14
  @description = description
15
15
  @icons = icons
16
16
  @mime_type = mime_type
17
+ @meta = meta
17
18
  end
18
19
 
19
20
  def to_h
@@ -24,6 +25,7 @@ module MCP
24
25
  description: description,
25
26
  icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
26
27
  mimeType: mime_type,
28
+ _meta: meta,
27
29
  }.compact
28
30
  end
29
31
  end
@@ -2,15 +2,16 @@
2
2
 
3
3
  module MCP
4
4
  class ResourceTemplate
5
- attr_reader :uri_template, :name, :title, :description, :icons, :mime_type
5
+ attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta
6
6
 
7
- def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil)
7
+ def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
8
8
  @uri_template = uri_template
9
9
  @name = name
10
10
  @title = title
11
11
  @description = description
12
12
  @icons = icons
13
13
  @mime_type = mime_type
14
+ @meta = meta
14
15
  end
15
16
 
16
17
  def to_h
@@ -21,6 +22,7 @@ module MCP
21
22
  description: description,
22
23
  icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
23
24
  mimeType: mime_type,
25
+ _meta: meta,
24
26
  }.compact
25
27
  end
26
28
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Server
5
+ module Pagination
6
+ private
7
+
8
+ def cursor_from(request)
9
+ return if request.nil?
10
+
11
+ unless request.is_a?(Hash)
12
+ raise RequestHandlerError.new("Invalid params", request, error_type: :invalid_params)
13
+ end
14
+
15
+ request[:cursor]
16
+ end
17
+
18
+ def paginate(items, cursor:, page_size:, request:, &block)
19
+ start_index = 0
20
+
21
+ if cursor
22
+ unless cursor.is_a?(String)
23
+ raise RequestHandlerError.new("Invalid cursor", request, error_type: :invalid_params)
24
+ end
25
+
26
+ start_index = Integer(cursor, exception: false)
27
+ if start_index.nil? || start_index < 0 || start_index >= items.size
28
+ raise RequestHandlerError.new("Invalid cursor", request, error_type: :invalid_params)
29
+ end
30
+ end
31
+
32
+ end_index = page_size ? start_index + page_size : items.size
33
+ page = items[start_index...end_index]
34
+ page = page.map(&block) if block
35
+
36
+ result = { items: page }
37
+ result[:next_cursor] = end_index.to_s if end_index < items.size
38
+ result
39
+ end
40
+ end
41
+ end
42
+ end
@@ -3,6 +3,15 @@
3
3
  require "json"
4
4
  require_relative "../../transport"
5
5
 
6
+ # This file is autoloaded only when `StreamableHTTPTransport` is referenced,
7
+ # so the `rack` dependency does not affect `StdioTransport` users.
8
+ begin
9
+ require "rack"
10
+ rescue LoadError
11
+ raise LoadError, "The 'rack' gem is required to use the StreamableHTTPTransport. " \
12
+ "Add it to your Gemfile: gem 'rack'"
13
+ end
14
+
6
15
  module MCP
7
16
  class Server
8
17
  module Transports
@@ -13,13 +22,14 @@ module MCP
13
22
  "Connection" => "keep-alive",
14
23
  }.freeze
15
24
 
16
- def initialize(server, stateless: false, session_idle_timeout: nil)
25
+ def initialize(server, stateless: false, enable_json_response: false, session_idle_timeout: nil)
17
26
  super(server)
18
27
  # Maps `session_id` to `{ get_sse_stream: stream_object, server_session: ServerSession, last_active_at: float_from_monotonic_clock }`.
19
28
  @sessions = {}
20
29
  @mutex = Mutex.new
21
30
 
22
31
  @stateless = stateless
32
+ @enable_json_response = enable_json_response
23
33
  @session_idle_timeout = session_idle_timeout
24
34
  @pending_responses = {}
25
35
 
@@ -34,11 +44,17 @@ module MCP
34
44
  start_reaper_thread if @session_idle_timeout
35
45
  end
36
46
 
37
- REQUIRED_POST_ACCEPT_TYPES = ["application/json", "text/event-stream"].freeze
47
+ REQUIRED_POST_ACCEPT_TYPES_SSE = ["application/json", "text/event-stream"].freeze
48
+ REQUIRED_POST_ACCEPT_TYPES_JSON = ["application/json"].freeze
38
49
  REQUIRED_GET_ACCEPT_TYPES = ["text/event-stream"].freeze
39
50
  STREAM_WRITE_ERRORS = [IOError, Errno::EPIPE, Errno::ECONNRESET].freeze
40
51
  SESSION_REAP_INTERVAL = 60
41
52
 
53
+ # Rack app interface. This transport can be mounted as a Rack app.
54
+ def call(env)
55
+ handle_request(Rack::Request.new(env))
56
+ end
57
+
42
58
  def handle_request(request)
43
59
  case request.env["REQUEST_METHOD"]
44
60
  when "POST"
@@ -80,6 +96,12 @@ module MCP
80
96
 
81
97
  result = @mutex.synchronize do
82
98
  if session_id
99
+ # JSON response mode returns a single JSON object as the POST response,
100
+ # so request-scoped notifications (e.g. progress, log) cannot be delivered
101
+ # alongside it. Session-scoped standalone notifications
102
+ # (e.g. `resources/updated`, `elicitation/complete`) still flow via GET SSE.
103
+ next false if @enable_json_response && related_request_id
104
+
83
105
  # Send to specific session
84
106
  if (session = @sessions[session_id])
85
107
  stream = active_stream(session, related_request_id: related_request_id)
@@ -158,6 +180,10 @@ module MCP
158
180
  raise "Stateless mode does not support server-to-client requests."
159
181
  end
160
182
 
183
+ if @enable_json_response
184
+ raise "JSON response mode does not support server-to-client requests."
185
+ end
186
+
161
187
  unless session_id
162
188
  raise "session_id is required for server-to-client requests."
163
189
  end
@@ -255,16 +281,17 @@ module MCP
255
281
  def send_to_stream(stream, data)
256
282
  message = data.is_a?(String) ? data : data.to_json
257
283
  stream.write("data: #{message}\n\n")
258
- stream.flush if stream.respond_to?(:flush)
284
+ stream.flush
259
285
  end
260
286
 
261
287
  def send_ping_to_stream(stream)
262
288
  stream.write(": ping #{Time.now.iso8601}\n\n")
263
- stream.flush if stream.respond_to?(:flush)
289
+ stream.flush
264
290
  end
265
291
 
266
292
  def handle_post(request)
267
- accept_error = validate_accept_header(request, REQUIRED_POST_ACCEPT_TYPES)
293
+ required_types = @enable_json_response ? REQUIRED_POST_ACCEPT_TYPES_JSON : REQUIRED_POST_ACCEPT_TYPES_SSE
294
+ accept_error = validate_accept_header(request, required_types)
268
295
  return accept_error if accept_error
269
296
 
270
297
  content_type_error = validate_content_type(request)
@@ -505,7 +532,7 @@ module MCP
505
532
  end
506
533
  end
507
534
 
508
- if session_id && !@stateless
535
+ if session_id && !@stateless && !@enable_json_response
509
536
  handle_request_with_sse_response(body_string, session_id, server_session, related_request_id: related_request_id)
510
537
  else
511
538
  response = dispatch_handle_json(body_string, server_session)
@@ -546,7 +573,7 @@ module MCP
546
573
  end
547
574
  end
548
575
 
549
- [200, SSE_HEADERS, body]
576
+ [200, SSE_HEADERS.dup, body]
550
577
  end
551
578
 
552
579
  # Returns the SSE stream available for server-to-client messages.
@@ -628,7 +655,7 @@ module MCP
628
655
  def setup_sse_stream(session_id)
629
656
  body = create_sse_body(session_id)
630
657
 
631
- [200, SSE_HEADERS, body]
658
+ [200, SSE_HEADERS.dup, body]
632
659
  end
633
660
 
634
661
  def create_sse_body(session_id)
data/lib/mcp/server.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "methods"
6
6
  require_relative "logging_message_notification"
7
7
  require_relative "progress"
8
8
  require_relative "server_context"
9
+ require_relative "server/pagination"
9
10
  require_relative "server/transports"
10
11
 
11
12
  module MCP
@@ -31,14 +32,27 @@ module MCP
31
32
  MAX_COMPLETION_VALUES = 100
32
33
 
33
34
  class RequestHandlerError < StandardError
34
- attr_reader :error_type
35
- attr_reader :original_error
35
+ attr_reader :error_type, :original_error, :error_code, :error_data
36
36
 
37
- def initialize(message, request, error_type: :internal_error, original_error: nil)
37
+ def initialize(message, request, error_type: :internal_error, original_error: nil, error_code: nil, error_data: nil)
38
38
  super(message)
39
39
  @request = request
40
40
  @error_type = error_type
41
41
  @original_error = original_error
42
+ @error_code = error_code
43
+ @error_data = error_data
44
+ end
45
+ end
46
+
47
+ class URLElicitationRequiredError < RequestHandlerError
48
+ def initialize(elicitations)
49
+ super(
50
+ "URL elicitation required",
51
+ nil,
52
+ error_type: :url_elicitation_required,
53
+ error_code: -32042,
54
+ error_data: { elicitations: elicitations },
55
+ )
42
56
  end
43
57
  end
44
58
 
@@ -52,9 +66,10 @@ module MCP
52
66
  end
53
67
 
54
68
  include Instrumentation
69
+ include Pagination
55
70
 
56
71
  attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
57
- attr_reader :client_capabilities
72
+ attr_reader :page_size, :client_capabilities
58
73
 
59
74
  def initialize(
60
75
  description: nil,
@@ -71,6 +86,7 @@ module MCP
71
86
  server_context: nil,
72
87
  configuration: nil,
73
88
  capabilities: nil,
89
+ page_size: nil,
74
90
  transport: nil
75
91
  )
76
92
  @description = description
@@ -87,6 +103,7 @@ module MCP
87
103
  @resource_templates = resource_templates
88
104
  @resource_index = index_resources_by_uri(resources)
89
105
  @server_context = server_context
106
+ self.page_size = page_size
90
107
  @configuration = MCP.configuration.merge(configuration)
91
108
  @client = nil
92
109
 
@@ -100,6 +117,8 @@ module MCP
100
117
  Methods::RESOURCES_LIST => method(:list_resources),
101
118
  Methods::RESOURCES_READ => method(:read_resource_no_content),
102
119
  Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
120
+ Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
121
+ Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
103
122
  Methods::TOOLS_LIST => method(:list_tools),
104
123
  Methods::TOOLS_CALL => method(:call_tool),
105
124
  Methods::PROMPTS_LIST => method(:list_prompts),
@@ -108,13 +127,9 @@ module MCP
108
127
  Methods::PING => ->(_) { {} },
109
128
  Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
110
129
  Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
130
+ Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED => ->(_) {},
111
131
  Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
112
132
  Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
113
-
114
- # No op handlers for currently unsupported methods
115
- Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
116
- Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
117
- Methods::ELICITATION_CREATE => ->(_) {},
118
133
  }
119
134
  @transport = transport
120
135
  end
@@ -170,6 +185,14 @@ module MCP
170
185
  @handlers[method_name] = block
171
186
  end
172
187
 
188
+ def page_size=(page_size)
189
+ unless page_size.nil? || (page_size.is_a?(Integer) && page_size > 0)
190
+ raise ArgumentError, "page_size must be nil or a positive integer"
191
+ end
192
+
193
+ @page_size = page_size
194
+ end
195
+
173
196
  def notify_tools_list_changed
174
197
  return unless @transport
175
198
 
@@ -206,42 +229,12 @@ module MCP
206
229
  report_exception(e, { notification: "log_message" })
207
230
  end
208
231
 
209
- # Sends a `sampling/createMessage` request to the client.
210
- # For single-client transports (e.g., `StdioTransport`). For multi-client transports
211
- # (e.g., `StreamableHTTPTransport`), use `ServerSession#create_sampling_message` instead
212
- # to ensure the request is routed to the correct client.
213
- def create_sampling_message(
214
- messages:,
215
- max_tokens:,
216
- system_prompt: nil,
217
- model_preferences: nil,
218
- include_context: nil,
219
- temperature: nil,
220
- stop_sequences: nil,
221
- metadata: nil,
222
- tools: nil,
223
- tool_choice: nil,
224
- related_request_id: nil
225
- )
226
- unless @transport
227
- raise "Cannot send sampling request without a transport."
228
- end
229
-
230
- params = build_sampling_params(
231
- @client_capabilities,
232
- messages: messages,
233
- max_tokens: max_tokens,
234
- system_prompt: system_prompt,
235
- model_preferences: model_preferences,
236
- include_context: include_context,
237
- temperature: temperature,
238
- stop_sequences: stop_sequences,
239
- metadata: metadata,
240
- tools: tools,
241
- tool_choice: tool_choice,
242
- )
243
-
244
- @transport.send_request(Methods::SAMPLING_CREATE_MESSAGE, params)
232
+ # Sets a handler for `notifications/roots/list_changed` notifications.
233
+ # Called when a client notifies the server that its filesystem roots have changed.
234
+ #
235
+ # @yield [params] The notification params (typically `nil`).
236
+ def roots_list_changed_handler(&block)
237
+ @handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
245
238
  end
246
239
 
247
240
  # Sets a custom handler for `resources/read` requests.
@@ -263,6 +256,24 @@ module MCP
263
256
  @handlers[Methods::COMPLETION_COMPLETE] = block
264
257
  end
265
258
 
259
+ # Sets a custom handler for `resources/subscribe` requests.
260
+ # The block receives the parsed request params. The return value is
261
+ # ignored; the response is always an empty result `{}` per the MCP specification.
262
+ #
263
+ # @yield [params] The request params containing `:uri`.
264
+ def resources_subscribe_handler(&block)
265
+ @handlers[Methods::RESOURCES_SUBSCRIBE] = block
266
+ end
267
+
268
+ # Sets a custom handler for `resources/unsubscribe` requests.
269
+ # The block receives the parsed request params. The return value is
270
+ # ignored; the response is always an empty result `{}` per the MCP specification.
271
+ #
272
+ # @yield [params] The request params containing `:uri`.
273
+ def resources_unsubscribe_handler(&block)
274
+ @handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
275
+ end
276
+
266
277
  def build_sampling_params(
267
278
  capabilities,
268
279
  messages:,
@@ -375,7 +386,7 @@ module MCP
375
386
  def handle_request(request, method, session: nil, related_request_id: nil)
376
387
  handler = @handlers[method]
377
388
  unless handler
378
- instrument_call("unsupported_method") do
389
+ instrument_call("unsupported_method", server_context: { request: request }) do
379
390
  client = session&.client || @client
380
391
  add_instrumentation_data(client: client) if client
381
392
  end
@@ -385,20 +396,20 @@ module MCP
385
396
  Methods.ensure_capability!(method, capabilities)
386
397
 
387
398
  ->(params) {
388
- instrument_call(method) do
399
+ reported_exception = nil
400
+ instrument_call(
401
+ method,
402
+ server_context: { request: request },
403
+ exception_already_reported: ->(e) { reported_exception.equal?(e) },
404
+ ) do
389
405
  result = case method
390
406
  when Methods::INITIALIZE
391
407
  init(params, session: session)
392
- when Methods::TOOLS_LIST
393
- { tools: @handlers[Methods::TOOLS_LIST].call(params) }
394
- when Methods::PROMPTS_LIST
395
- { prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
396
- when Methods::RESOURCES_LIST
397
- { resources: @handlers[Methods::RESOURCES_LIST].call(params) }
398
408
  when Methods::RESOURCES_READ
399
409
  { contents: @handlers[Methods::RESOURCES_READ].call(params) }
400
- when Methods::RESOURCES_TEMPLATES_LIST
401
- { resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
410
+ when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
411
+ @handlers[method].call(params)
412
+ {}
402
413
  when Methods::TOOLS_CALL
403
414
  call_tool(params, session: session, related_request_id: related_request_id)
404
415
  when Methods::COMPLETION_COMPLETE
@@ -415,11 +426,14 @@ module MCP
415
426
  rescue RequestHandlerError => e
416
427
  report_exception(e.original_error || e, { request: request })
417
428
  add_instrumentation_data(error: e.error_type)
429
+ reported_exception = e
418
430
  raise e
419
431
  rescue => e
420
432
  report_exception(e, { request: request })
421
433
  add_instrumentation_data(error: :internal_error)
422
- raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
434
+ wrapped = RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
435
+ reported_exception = wrapped
436
+ raise wrapped
423
437
  end
424
438
  }
425
439
  end
@@ -497,7 +511,9 @@ module MCP
497
511
  end
498
512
 
499
513
  def list_tools(request)
500
- @tools.values.map(&:to_h)
514
+ page = paginate(@tools.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
515
+
516
+ { tools: page[:items], nextCursor: page[:next_cursor] }.compact
501
517
  end
502
518
 
503
519
  def call_tool(request, session: nil, related_request_id: nil)
@@ -545,7 +561,9 @@ module MCP
545
561
  end
546
562
 
547
563
  def list_prompts(request)
548
- @prompts.values.map(&:to_h)
564
+ page = paginate(@prompts.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
565
+
566
+ { prompts: page[:items], nextCursor: page[:next_cursor] }.compact
549
567
  end
550
568
 
551
569
  def get_prompt(request)
@@ -565,7 +583,9 @@ module MCP
565
583
  end
566
584
 
567
585
  def list_resources(request)
568
- @resources.map(&:to_h)
586
+ page = paginate(@resources, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
587
+
588
+ { resources: page[:items], nextCursor: page[:next_cursor] }.compact
569
589
  end
570
590
 
571
591
  # Server implementation should set `resources_read_handler` to override no-op default
@@ -575,7 +595,9 @@ module MCP
575
595
  end
576
596
 
577
597
  def list_resource_templates(request)
578
- @resource_templates.map(&:to_h)
598
+ page = paginate(@resource_templates, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
599
+
600
+ { resourceTemplates: page[:items], nextCursor: page[:next_cursor] }.compact
579
601
  end
580
602
 
581
603
  def complete(params)