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/server.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../json_rpc_handler"
4
+ require_relative "cancellation"
5
+ require_relative "cancelled_error"
4
6
  require_relative "instrumentation"
5
7
  require_relative "methods"
6
8
  require_relative "logging_message_notification"
7
9
  require_relative "progress"
8
10
  require_relative "server_context"
11
+ require_relative "server/pagination"
9
12
  require_relative "server/transports"
10
13
 
11
14
  module MCP
@@ -24,15 +27,34 @@ module MCP
24
27
  UNSUPPORTED_PROPERTIES_UNTIL_2025_06_18 = [:description, :icons].freeze
25
28
  UNSUPPORTED_PROPERTIES_UNTIL_2025_03_26 = [:title, :websiteUrl].freeze
26
29
 
30
+ DEFAULT_COMPLETION_RESULT = { completion: { values: [], hasMore: false } }.freeze
31
+
32
+ # Servers return an array of completion values ranked by relevance, with maximum 100 items per response.
33
+ # https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion#completion-results
34
+ MAX_COMPLETION_VALUES = 100
35
+
27
36
  class RequestHandlerError < StandardError
28
- attr_reader :error_type
29
- attr_reader :original_error
37
+ attr_reader :error_type, :original_error, :error_code, :error_data
30
38
 
31
- def initialize(message, request, error_type: :internal_error, original_error: nil)
39
+ def initialize(message, request, error_type: :internal_error, original_error: nil, error_code: nil, error_data: nil)
32
40
  super(message)
33
41
  @request = request
34
42
  @error_type = error_type
35
43
  @original_error = original_error
44
+ @error_code = error_code
45
+ @error_data = error_data
46
+ end
47
+ end
48
+
49
+ class URLElicitationRequiredError < RequestHandlerError
50
+ def initialize(elicitations)
51
+ super(
52
+ "URL elicitation required",
53
+ nil,
54
+ error_type: :url_elicitation_required,
55
+ error_code: -32042,
56
+ error_data: { elicitations: elicitations },
57
+ )
36
58
  end
37
59
  end
38
60
 
@@ -46,8 +68,10 @@ module MCP
46
68
  end
47
69
 
48
70
  include Instrumentation
71
+ include Pagination
49
72
 
50
73
  attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
74
+ attr_reader :page_size, :client_capabilities
51
75
 
52
76
  def initialize(
53
77
  description: nil,
@@ -64,6 +88,7 @@ module MCP
64
88
  server_context: nil,
65
89
  configuration: nil,
66
90
  capabilities: nil,
91
+ page_size: nil,
67
92
  transport: nil
68
93
  )
69
94
  @description = description
@@ -80,18 +105,22 @@ module MCP
80
105
  @resource_templates = resource_templates
81
106
  @resource_index = index_resources_by_uri(resources)
82
107
  @server_context = server_context
108
+ self.page_size = page_size
83
109
  @configuration = MCP.configuration.merge(configuration)
84
110
  @client = nil
85
111
 
86
112
  validate!
87
113
 
88
114
  @capabilities = capabilities || default_capabilities
115
+ @client_capabilities = nil
89
116
  @logging_message_notification = nil
90
117
 
91
118
  @handlers = {
92
119
  Methods::RESOURCES_LIST => method(:list_resources),
93
120
  Methods::RESOURCES_READ => method(:read_resource_no_content),
94
121
  Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
122
+ Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
123
+ Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
95
124
  Methods::TOOLS_LIST => method(:list_tools),
96
125
  Methods::TOOLS_CALL => method(:call_tool),
97
126
  Methods::PROMPTS_LIST => method(:list_prompts),
@@ -100,13 +129,9 @@ module MCP
100
129
  Methods::PING => ->(_) { {} },
101
130
  Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
102
131
  Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
132
+ Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED => ->(_) {},
133
+ Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
103
134
  Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
104
-
105
- # No op handlers for currently unsupported methods
106
- Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
107
- Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
108
- Methods::COMPLETION_COMPLETE => ->(_) { { completion: { values: [], hasMore: false } } },
109
- Methods::ELICITATION_CREATE => ->(_) {},
110
135
  }
111
136
  @transport = transport
112
137
  end
@@ -119,8 +144,8 @@ module MCP
119
144
  # When `nil`, progress and logging notifications from tool handlers are silently skipped.
120
145
  # @return [Hash, nil] The JSON-RPC response, or `nil` for notifications.
121
146
  def handle(request, session: nil)
122
- JsonRpcHandler.handle(request) do |method|
123
- handle_request(request, method, session: session)
147
+ JsonRpcHandler.handle(request) do |method, request_id|
148
+ handle_request(request, method, session: session, related_request_id: request_id)
124
149
  end
125
150
  end
126
151
 
@@ -132,8 +157,8 @@ module MCP
132
157
  # When `nil`, progress and logging notifications from tool handlers are silently skipped.
133
158
  # @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
134
159
  def handle_json(request, session: nil)
135
- JsonRpcHandler.handle_json(request) do |method|
136
- handle_request(request, method, session: session)
160
+ JsonRpcHandler.handle_json(request) do |method, request_id|
161
+ handle_request(request, method, session: session, related_request_id: request_id)
137
162
  end
138
163
  end
139
164
 
@@ -162,6 +187,14 @@ module MCP
162
187
  @handlers[method_name] = block
163
188
  end
164
189
 
190
+ def page_size=(page_size)
191
+ unless page_size.nil? || (page_size.is_a?(Integer) && page_size > 0)
192
+ raise ArgumentError, "page_size must be nil or a positive integer"
193
+ end
194
+
195
+ @page_size = page_size
196
+ end
197
+
165
198
  def notify_tools_list_changed
166
199
  return unless @transport
167
200
 
@@ -198,6 +231,14 @@ module MCP
198
231
  report_exception(e, { notification: "log_message" })
199
232
  end
200
233
 
234
+ # Sets a handler for `notifications/roots/list_changed` notifications.
235
+ # Called when a client notifies the server that its filesystem roots have changed.
236
+ #
237
+ # @yield [params] The notification params (typically `nil`).
238
+ def roots_list_changed_handler(&block)
239
+ @handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
240
+ end
241
+
201
242
  # Sets a custom handler for `resources/read` requests.
202
243
  # The block receives the parsed request params and should return resource
203
244
  # contents. The return value is set as the `contents` field of the response.
@@ -208,6 +249,72 @@ module MCP
208
249
  @handlers[Methods::RESOURCES_READ] = block
209
250
  end
210
251
 
252
+ # Sets a custom handler for `completion/complete` requests.
253
+ # The block receives the parsed request params and should return completion values.
254
+ #
255
+ # @yield [params] The request params containing `:ref`, `:argument`, and optionally `:context`.
256
+ # @yieldreturn [Hash] A hash with `:completion` key containing `:values`, optional `:total`, and `:hasMore`.
257
+ def completion_handler(&block)
258
+ @handlers[Methods::COMPLETION_COMPLETE] = block
259
+ end
260
+
261
+ # Sets a custom handler for `resources/subscribe` requests.
262
+ # The block receives the parsed request params. The return value is
263
+ # ignored; the response is always an empty result `{}` per the MCP specification.
264
+ #
265
+ # @yield [params] The request params containing `:uri`.
266
+ def resources_subscribe_handler(&block)
267
+ @handlers[Methods::RESOURCES_SUBSCRIBE] = block
268
+ end
269
+
270
+ # Sets a custom handler for `resources/unsubscribe` requests.
271
+ # The block receives the parsed request params. The return value is
272
+ # ignored; the response is always an empty result `{}` per the MCP specification.
273
+ #
274
+ # @yield [params] The request params containing `:uri`.
275
+ def resources_unsubscribe_handler(&block)
276
+ @handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
277
+ end
278
+
279
+ def build_sampling_params(
280
+ capabilities,
281
+ messages:,
282
+ max_tokens:,
283
+ system_prompt: nil,
284
+ model_preferences: nil,
285
+ include_context: nil,
286
+ temperature: nil,
287
+ stop_sequences: nil,
288
+ metadata: nil,
289
+ tools: nil,
290
+ tool_choice: nil
291
+ )
292
+ unless capabilities&.dig(:sampling)
293
+ raise "Client does not support sampling."
294
+ end
295
+
296
+ if tools && !capabilities.dig(:sampling, :tools)
297
+ raise "Client does not support sampling with tools."
298
+ end
299
+
300
+ if tool_choice && !capabilities.dig(:sampling, :tools)
301
+ raise "Client does not support sampling with tool_choice."
302
+ end
303
+
304
+ {
305
+ messages: messages,
306
+ maxTokens: max_tokens,
307
+ systemPrompt: system_prompt,
308
+ modelPreferences: model_preferences,
309
+ includeContext: include_context,
310
+ temperature: temperature,
311
+ stopSequences: stop_sequences,
312
+ metadata: metadata,
313
+ tools: tools,
314
+ toolChoice: tool_choice,
315
+ }.compact
316
+ end
317
+
211
318
  private
212
319
 
213
320
  def validate!
@@ -278,10 +385,17 @@ module MCP
278
385
  end
279
386
  end
280
387
 
281
- def handle_request(request, method, session: nil)
388
+ def handle_request(request, method, session: nil, related_request_id: nil)
389
+ # `notifications/cancelled` is dispatched directly: it is a notification (no JSON-RPC id)
390
+ # and intentionally bypasses the `@handlers` lookup, capability check, in-flight registry,
391
+ # and rescue blocks below.
392
+ if method == Methods::NOTIFICATIONS_CANCELLED
393
+ return ->(params) { handle_cancelled_notification(params, session: session) }
394
+ end
395
+
282
396
  handler = @handlers[method]
283
397
  unless handler
284
- instrument_call("unsupported_method") do
398
+ instrument_call("unsupported_method", server_context: { request: request }) do
285
399
  client = session&.client || @client
286
400
  add_instrumentation_data(client: client) if client
287
401
  end
@@ -290,45 +404,78 @@ module MCP
290
404
 
291
405
  Methods.ensure_capability!(method, capabilities)
292
406
 
407
+ # `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2),
408
+ # so do not track it in the in-flight registry.
409
+ cancellation = if related_request_id && method != Methods::INITIALIZE
410
+ session&.register_in_flight(related_request_id)
411
+ end
412
+
293
413
  ->(params) {
294
- instrument_call(method) do
414
+ reported_exception = nil
415
+ instrument_call(
416
+ method,
417
+ server_context: { request: request },
418
+ exception_already_reported: ->(e) { reported_exception.equal?(e) },
419
+ ) do
295
420
  result = case method
296
421
  when Methods::INITIALIZE
297
422
  init(params, session: session)
298
- when Methods::TOOLS_LIST
299
- { tools: @handlers[Methods::TOOLS_LIST].call(params) }
300
- when Methods::PROMPTS_LIST
301
- { prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
302
- when Methods::RESOURCES_LIST
303
- { resources: @handlers[Methods::RESOURCES_LIST].call(params) }
304
423
  when Methods::RESOURCES_READ
305
- { contents: @handlers[Methods::RESOURCES_READ].call(params) }
306
- when Methods::RESOURCES_TEMPLATES_LIST
307
- { resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
424
+ { contents: read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation) }
425
+ when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
426
+ dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation)
427
+ {}
308
428
  when Methods::TOOLS_CALL
309
- call_tool(params, session: session)
429
+ call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation)
430
+ when Methods::PROMPTS_GET
431
+ get_prompt(params, session: session, related_request_id: related_request_id, cancellation: cancellation)
432
+ when Methods::COMPLETION_COMPLETE
433
+ complete(params, session: session, related_request_id: related_request_id, cancellation: cancellation)
310
434
  when Methods::LOGGING_SET_LEVEL
311
435
  configure_logging_level(params, session: session)
312
436
  else
313
- @handlers[method].call(params)
437
+ dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation)
314
438
  end
315
439
  client = session&.client || @client
316
440
  add_instrumentation_data(client: client) if client
317
441
 
442
+ if cancellation&.cancelled?
443
+ add_instrumentation_data(cancelled: true, cancellation_reason: cancellation.reason)
444
+ next JsonRpcHandler::NO_RESPONSE
445
+ end
446
+
318
447
  result
448
+ rescue CancelledError => e
449
+ add_instrumentation_data(cancelled: true, cancellation_reason: e.reason)
450
+ next JsonRpcHandler::NO_RESPONSE
451
+ rescue RequestHandlerError => e
452
+ report_exception(e.original_error || e, { request: request })
453
+ add_instrumentation_data(error: e.error_type)
454
+ reported_exception = e
455
+ raise e
319
456
  rescue => e
320
457
  report_exception(e, { request: request })
321
- if e.is_a?(RequestHandlerError)
322
- add_instrumentation_data(error: e.error_type)
323
- raise e
324
- end
325
-
326
458
  add_instrumentation_data(error: :internal_error)
327
- raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
459
+ wrapped = RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
460
+ reported_exception = wrapped
461
+ raise wrapped
462
+ ensure
463
+ session&.unregister_in_flight(related_request_id) if related_request_id
328
464
  end
329
465
  }
330
466
  end
331
467
 
468
+ def handle_cancelled_notification(params, session: nil)
469
+ return unless session
470
+ return unless params.is_a?(Hash)
471
+
472
+ request_id = params[:requestId] || params["requestId"]
473
+ return if request_id.nil?
474
+
475
+ reason = params[:reason] || params["reason"]
476
+ session.cancel_incoming(request_id: request_id, reason: reason)
477
+ end
478
+
332
479
  def default_capabilities
333
480
  {
334
481
  tools: { listChanged: true },
@@ -355,10 +502,11 @@ module MCP
355
502
  session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
356
503
  else
357
504
  @client = params[:clientInfo]
505
+ @client_capabilities = params[:capabilities]
358
506
  end
507
+ protocol_version = params[:protocolVersion]
359
508
  end
360
509
 
361
- protocol_version = params[:protocolVersion] if params
362
510
  negotiated_version = if Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
363
511
  protocol_version
364
512
  else
@@ -401,10 +549,12 @@ module MCP
401
549
  end
402
550
 
403
551
  def list_tools(request)
404
- @tools.values.map(&:to_h)
552
+ page = paginate(@tools.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
553
+
554
+ { tools: page[:items], nextCursor: page[:next_cursor] }.compact
405
555
  end
406
556
 
407
- def call_tool(request, session: nil)
557
+ def call_tool(request, session: nil, related_request_id: nil, cancellation: nil)
408
558
  tool_name = request[:name]
409
559
 
410
560
  tool = tools[tool_name]
@@ -436,20 +586,29 @@ module MCP
436
586
 
437
587
  progress_token = request.dig(:_meta, :progressToken)
438
588
 
439
- call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token, session: session)
440
- rescue RequestHandlerError
589
+ call_tool_with_args(
590
+ tool, arguments, server_context_with_meta(request), progress_token: progress_token, session: session, related_request_id: related_request_id, cancellation: cancellation
591
+ )
592
+ rescue RequestHandlerError, CancelledError
593
+ # CancelledError is intentionally not wrapped so `handle_request` can turn it into
594
+ # `JsonRpcHandler::NO_RESPONSE` per the MCP cancellation spec.
441
595
  raise
442
596
  rescue => e
443
- report_exception(e, request: request)
444
-
445
- error_tool_response("Internal error calling tool #{tool_name}: #{e.message}")
597
+ raise RequestHandlerError.new(
598
+ "Internal error calling tool #{tool_name}: #{e.message}",
599
+ request,
600
+ error_type: :internal_error,
601
+ original_error: e,
602
+ )
446
603
  end
447
604
 
448
605
  def list_prompts(request)
449
- @prompts.values.map(&:to_h)
606
+ page = paginate(@prompts.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
607
+
608
+ { prompts: page[:items], nextCursor: page[:next_cursor] }.compact
450
609
  end
451
610
 
452
- def get_prompt(request)
611
+ def get_prompt(request, session: nil, related_request_id: nil, cancellation: nil)
453
612
  prompt_name = request[:name]
454
613
  prompt = @prompts[prompt_name]
455
614
  unless prompt
@@ -462,11 +621,20 @@ module MCP
462
621
  prompt_args = request[:arguments]
463
622
  prompt.validate_arguments!(prompt_args)
464
623
 
465
- call_prompt_template_with_args(prompt, prompt_args, server_context_with_meta(request))
624
+ server_context = build_server_context(
625
+ request: request,
626
+ session: session,
627
+ related_request_id: related_request_id,
628
+ cancellation: cancellation,
629
+ )
630
+
631
+ call_prompt_template_with_args(prompt, prompt_args, server_context)
466
632
  end
467
633
 
468
634
  def list_resources(request)
469
- @resources.map(&:to_h)
635
+ page = paginate(@resources, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
636
+
637
+ { resources: page[:items], nextCursor: page[:next_cursor] }.compact
470
638
  end
471
639
 
472
640
  # Server implementation should set `resources_read_handler` to override no-op default
@@ -476,7 +644,85 @@ module MCP
476
644
  end
477
645
 
478
646
  def list_resource_templates(request)
479
- @resource_templates.map(&:to_h)
647
+ page = paginate(@resource_templates, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
648
+
649
+ { resourceTemplates: page[:items], nextCursor: page[:next_cursor] }.compact
650
+ end
651
+
652
+ def complete(params, session: nil, related_request_id: nil, cancellation: nil)
653
+ validate_completion_params!(params)
654
+
655
+ result = dispatch_optional_context_handler(
656
+ @handlers[Methods::COMPLETION_COMPLETE],
657
+ params,
658
+ session: session,
659
+ related_request_id: related_request_id,
660
+ cancellation: cancellation,
661
+ )
662
+
663
+ normalize_completion_result(result)
664
+ end
665
+
666
+ # Invokes `resources/read` via the registered handler. If the handler block opts in to `server_context:`,
667
+ # pass an `MCP::ServerContext` so the handler can observe cancellation via `server_context.cancelled?` or
668
+ # `server_context.raise_if_cancelled!`.
669
+ def read_resource_contents(request, session: nil, related_request_id: nil, cancellation: nil)
670
+ dispatch_optional_context_handler(
671
+ @handlers[Methods::RESOURCES_READ],
672
+ request,
673
+ session: session,
674
+ related_request_id: related_request_id,
675
+ cancellation: cancellation,
676
+ )
677
+ end
678
+
679
+ # Opt-in `server_context:` dispatch for block-based handlers registered via `resources_read_handler`,
680
+ # `completion_handler`, `resources_subscribe_handler`, `resources_unsubscribe_handler`, or `define_custom_method`.
681
+ # Existing handlers that only accept `params` are called unchanged; handlers that declare a `server_context:`
682
+ # keyword receive an `MCP::ServerContext` wrapping the raw server context with cancellation plumbing.
683
+ def dispatch_optional_context_handler(handler, params, session: nil, related_request_id: nil, cancellation: nil)
684
+ return handler.call(params) unless handler_declares_server_context?(handler)
685
+
686
+ server_context = build_server_context(
687
+ request: params,
688
+ session: session,
689
+ related_request_id: related_request_id,
690
+ cancellation: cancellation,
691
+ )
692
+ handler.call(params, server_context: server_context)
693
+ end
694
+
695
+ # Stricter than `accepts_server_context?`: requires `server_context` to appear as a named keyword parameter
696
+ # (`:key` optional, `:keyreq` required). Positional parameters named `server_context` (`:req` / `:opt`) are NOT
697
+ # treated as opt-in - otherwise `handler.call(params, server_context: ctx)` would pass the `{server_context: ctx}`
698
+ # Hash as the handler's second positional argument, which is never what the user meant.
699
+ #
700
+ # `**kwargs`-only signatures (`:keyrest` without a named `server_context`) are also not opt-in here,
701
+ # because the dispatch site passes a positional `params`, and a `**kwargs`-only block cannot accept
702
+ # that positional argument (lambdas/methods raise `ArgumentError`; non-lambda procs silently drop `params`).
703
+ # Tool handlers intentionally allow `**kwargs` opt-in via `accepts_server_context?` because they are invoked
704
+ # via `tool.call(**args, server_context: …)` without a positional argument.
705
+ def handler_declares_server_context?(handler)
706
+ return false unless handler.respond_to?(:parameters)
707
+
708
+ handler.parameters.any? do |type, name|
709
+ name == :server_context && (type == :key || type == :keyreq)
710
+ end
711
+ end
712
+
713
+ # Builds an `MCP::ServerContext` used to give a handler access to session-scoped helpers
714
+ # (progress, cancellation, nested server-to-client requests).
715
+ def build_server_context(request:, session:, related_request_id:, cancellation:)
716
+ meta_source = request.is_a?(Hash) ? request : {}
717
+ progress_token = meta_source.dig(:_meta, :progressToken)
718
+ progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
719
+ ServerContext.new(
720
+ server_context_with_meta(meta_source),
721
+ progress: progress,
722
+ notification_target: session,
723
+ related_request_id: related_request_id,
724
+ cancellation: cancellation,
725
+ )
480
726
  end
481
727
 
482
728
  def report_exception(exception, server_context = {})
@@ -499,18 +745,32 @@ module MCP
499
745
  ).to_h
500
746
  end
501
747
 
748
+ # Whether a tool/prompt handler opts in to receiving an `MCP::ServerContext`.
749
+ # Recognizes `:keyrest` (`**kwargs`) because tools are invoked without a positional argument
750
+ # (`tool.call(**args, server_context:)`), soa `**kwargs`-only signature safely captures `server_context:`.
751
+ # Named keyword `server_context` must be `:key` or `:keyreq` - positional parameters (`:req` / `:opt`) that
752
+ # happen to be named `server_context` are excluded because the call site passes `server_context:` as a keyword,
753
+ # and a positional slot would receive the `{server_context: ctx}` Hash instead.
502
754
  def accepts_server_context?(method_object)
503
755
  parameters = method_object.parameters
504
756
 
505
- parameters.any? { |type, name| type == :keyrest || name == :server_context }
757
+ parameters.any? do |type, name|
758
+ type == :keyrest || (name == :server_context && (type == :key || type == :keyreq))
759
+ end
506
760
  end
507
761
 
508
- def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil)
762
+ def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil, related_request_id: nil, cancellation: nil)
509
763
  args = arguments&.transform_keys(&:to_sym) || {}
510
764
 
511
765
  if accepts_server_context?(tool.method(:call))
512
- progress = Progress.new(notification_target: session, progress_token: progress_token)
513
- server_context = ServerContext.new(context, progress: progress, notification_target: session)
766
+ progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
767
+ server_context = ServerContext.new(
768
+ context,
769
+ progress: progress,
770
+ notification_target: session,
771
+ related_request_id: related_request_id,
772
+ cancellation: cancellation,
773
+ )
514
774
  tool.call(**args, server_context: server_context).to_h
515
775
  else
516
776
  tool.call(**args).to_h
@@ -537,5 +797,56 @@ module MCP
537
797
  server_context
538
798
  end
539
799
  end
800
+
801
+ def validate_completion_params!(params)
802
+ unless params.is_a?(Hash)
803
+ raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)
804
+ end
805
+
806
+ ref = params[:ref]
807
+ if ref.nil? || ref[:type].nil?
808
+ raise RequestHandlerError.new("Missing or invalid ref", params, error_type: :invalid_params)
809
+ end
810
+
811
+ argument = params[:argument]
812
+ if argument.nil? || argument[:name].nil? || !argument.key?(:value)
813
+ raise RequestHandlerError.new("Missing argument name or value", params, error_type: :invalid_params)
814
+ end
815
+
816
+ case ref[:type]
817
+ when "ref/prompt"
818
+ unless @prompts[ref[:name]]
819
+ raise RequestHandlerError.new("Prompt not found: #{ref[:name]}", params, error_type: :invalid_params)
820
+ end
821
+ when "ref/resource"
822
+ uri = ref[:uri]
823
+ found = @resource_index.key?(uri) || @resource_templates.any? { |t| t.uri_template == uri }
824
+ unless found
825
+ raise RequestHandlerError.new("Resource not found: #{uri}", params, error_type: :invalid_params)
826
+ end
827
+ else
828
+ raise RequestHandlerError.new("Invalid ref type: #{ref[:type]}", params, error_type: :invalid_params)
829
+ end
830
+ end
831
+
832
+ def normalize_completion_result(result)
833
+ return DEFAULT_COMPLETION_RESULT unless result.is_a?(Hash)
834
+
835
+ completion = result[:completion] || result["completion"]
836
+ return DEFAULT_COMPLETION_RESULT unless completion.is_a?(Hash)
837
+
838
+ values = completion[:values] || completion["values"] || []
839
+ total = completion[:total] || completion["total"]
840
+ has_more = completion[:hasMore] || completion["hasMore"] || false
841
+
842
+ count = values.length
843
+ if count > MAX_COMPLETION_VALUES
844
+ has_more = true
845
+ total ||= count
846
+ values = values.first(MAX_COMPLETION_VALUES)
847
+ end
848
+
849
+ { completion: { values: values, total: total, hasMore: has_more }.compact }
850
+ end
540
851
  end
541
852
  end