mcp 0.22.0 → 0.24.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
@@ -102,8 +102,11 @@ module MCP
102
102
  include Instrumentation
103
103
  include Pagination
104
104
 
105
- attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
106
- attr_reader :page_size, :client_capabilities
105
+ # Allowed values for the SEP-2549 `cacheScope` cache hint.
106
+ CACHE_SCOPES = ["public", "private"].freeze
107
+
108
+ attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resource_templates, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
109
+ attr_reader :resources, :page_size, :client_capabilities, :ttl_ms, :cache_scope
107
110
 
108
111
  def initialize(
109
112
  description: nil,
@@ -121,6 +124,8 @@ module MCP
121
124
  configuration: nil,
122
125
  capabilities: nil,
123
126
  page_size: nil,
127
+ ttl_ms: nil,
128
+ cache_scope: nil,
124
129
  transport: nil
125
130
  )
126
131
  @description = description
@@ -138,6 +143,8 @@ module MCP
138
143
  @resource_index = index_resources_by_uri(resources)
139
144
  @server_context = server_context
140
145
  self.page_size = page_size
146
+ self.ttl_ms = ttl_ms
147
+ self.cache_scope = cache_scope
141
148
  @configuration = MCP.configuration.merge(configuration)
142
149
  @client = nil
143
150
 
@@ -154,7 +161,7 @@ module MCP
154
161
 
155
162
  @handlers = {
156
163
  Methods::RESOURCES_LIST => method(:list_resources),
157
- Methods::RESOURCES_READ => method(:read_resource_no_content),
164
+ Methods::RESOURCES_READ => method(:read_resource),
158
165
  Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
159
166
  Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
160
167
  Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
@@ -163,6 +170,7 @@ module MCP
163
170
  Methods::PROMPTS_LIST => method(:list_prompts),
164
171
  Methods::PROMPTS_GET => method(:get_prompt),
165
172
  Methods::INITIALIZE => method(:init),
173
+ Methods::SERVER_DISCOVER => method(:discover),
166
174
  Methods::PING => ->(_) { {} },
167
175
  Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
168
176
  Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
@@ -216,6 +224,34 @@ module MCP
216
224
  validate!
217
225
  end
218
226
 
227
+ def define_resource(uri: nil, name: nil, title: nil, description: nil, icons: [], mime_type: nil, annotations: nil, size: nil, meta: nil, &block)
228
+ resource = Resource.define(
229
+ uri: uri, name: name, title: title, description: description, icons: icons,
230
+ mime_type: mime_type, annotations: annotations, size: size, meta: meta,
231
+ &block
232
+ )
233
+ @resources << resource
234
+ @resource_index[resource.uri] = resource
235
+
236
+ validate!
237
+ end
238
+
239
+ def define_resource_template(uri_template: nil, name: nil, title: nil, description: nil, icons: [], mime_type: nil, annotations: nil, meta: nil, &block)
240
+ resource_template = ResourceTemplate.define(
241
+ uri_template: uri_template, name: name, title: title, description: description, icons: icons,
242
+ mime_type: mime_type, annotations: annotations, meta: meta,
243
+ &block
244
+ )
245
+ @resource_templates << resource_template
246
+
247
+ validate!
248
+ end
249
+
250
+ def resources=(resources)
251
+ @resources = resources
252
+ @resource_index = index_resources_by_uri(resources)
253
+ end
254
+
219
255
  def define_custom_method(method_name:, &block)
220
256
  if @handlers.key?(method_name)
221
257
  raise MethodAlreadyDefinedError, method_name
@@ -232,6 +268,27 @@ module MCP
232
268
  @page_size = page_size
233
269
  end
234
270
 
271
+ # SEP-2549 cache hint: freshness lifetime in milliseconds for list and read results
272
+ # (max-age semantics; 0 means do not cache). Emission is opt-in: when both `ttl_ms`
273
+ # and `cache_scope` are nil, results are serialized exactly as before.
274
+ def ttl_ms=(ttl_ms)
275
+ unless ttl_ms.nil? || (ttl_ms.is_a?(Integer) && ttl_ms >= 0)
276
+ raise ArgumentError, "ttl_ms must be nil or a non-negative integer"
277
+ end
278
+
279
+ @ttl_ms = ttl_ms
280
+ end
281
+
282
+ # SEP-2549 cache hint: whether shared intermediaries may cache the result ("public")
283
+ # or only the requesting client ("private").
284
+ def cache_scope=(cache_scope)
285
+ unless cache_scope.nil? || CACHE_SCOPES.include?(cache_scope)
286
+ raise ArgumentError, "cache_scope must be nil, \"public\", or \"private\""
287
+ end
288
+
289
+ @cache_scope = cache_scope
290
+ end
291
+
235
292
  def notify_tools_list_changed
236
293
  return unless @transport
237
294
 
@@ -256,6 +313,9 @@ module MCP
256
313
  report_exception(e, { notification: "resources_list_changed" })
257
314
  end
258
315
 
316
+ # @deprecated MCP Logging (`logging/setLevel` and `notifications/message`)
317
+ # is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
318
+ # Use stderr or OpenTelemetry instead.
259
319
  def notify_log_message(data:, level:, logger: nil)
260
320
  return unless @transport
261
321
  return unless logging_message_notification&.should_notify?(level)
@@ -272,6 +332,10 @@ module MCP
272
332
  # Called when a client notifies the server that its filesystem roots have changed.
273
333
  #
274
334
  # @yield [params] The notification params (typically `nil`).
335
+ # @deprecated MCP Roots (`roots/list` and
336
+ # `notifications/roots/list_changed`) is deprecated as of MCP protocol
337
+ # version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
338
+ # server configuration, or environment variables instead.
275
339
  def roots_list_changed_handler(&block)
276
340
  @handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
277
341
  end
@@ -423,6 +487,13 @@ module MCP
423
487
  end
424
488
 
425
489
  def handle_request(request, method, session: nil, related_request_id: nil)
490
+ # A well-formed notification carries no JSON-RPC id and receives no response.
491
+ # If a client erroneously sends a notification-only method with an id, the message
492
+ # is framed as a request; since notification methods have no request handler,
493
+ # returning `nil` here makes `JsonRpcHandler` report "Method not found", matching
494
+ # the TypeScript and Python SDKs rather than emitting a spurious `result: null`.
495
+ return if Methods.notification?(method) && !related_request_id.nil?
496
+
426
497
  # `notifications/cancelled` is dispatched directly: it is a notification (no JSON-RPC id)
427
498
  # and intentionally bypasses the `@handlers` lookup, capability check, in-flight registry,
428
499
  # and rescue blocks below.
@@ -458,8 +529,9 @@ module MCP
458
529
  when Methods::INITIALIZE
459
530
  init(params, session: session)
460
531
  when Methods::RESOURCES_READ
461
- { contents: read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation) }
532
+ build_read_resource_result(read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation))
462
533
  when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
534
+ validate_resource_subscription_params!(params)
463
535
  dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation)
464
536
  {}
465
537
  when Methods::TOOLS_CALL
@@ -534,6 +606,8 @@ module MCP
534
606
  end
535
607
 
536
608
  def init(params, session: nil)
609
+ validate_initialize_params!(params)
610
+
537
611
  # MCP spec: the initialization phase MUST be the first interaction between client and server.
538
612
  # Reject duplicate `initialize` on an already-initialized session so the negotiated
539
613
  # client identity and capabilities cannot be silently overwritten.
@@ -541,15 +615,13 @@ module MCP
541
615
  raise RequestHandlerError.new("Invalid Request: Server already initialized", params, error_type: :invalid_request)
542
616
  end
543
617
 
544
- if params
545
- if session
546
- session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
547
- else
548
- @client = params[:clientInfo]
549
- @client_capabilities = params[:capabilities]
550
- end
551
- protocol_version = params[:protocolVersion]
618
+ if session
619
+ session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
620
+ else
621
+ @client = params[:clientInfo]
622
+ @client_capabilities = params[:capabilities]
552
623
  end
624
+ protocol_version = params[:protocolVersion]
553
625
 
554
626
  negotiated_version = if Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
555
627
  protocol_version
@@ -578,6 +650,47 @@ module MCP
578
650
  }.compact
579
651
  end
580
652
 
653
+ def validate_resource_subscription_params!(params)
654
+ unless params.is_a?(Hash) && params[:uri].is_a?(String)
655
+ raise RequestHandlerError.new("Missing or invalid uri", params, error_type: :invalid_params)
656
+ end
657
+ end
658
+
659
+ def validate_initialize_params!(params)
660
+ unless params.is_a?(Hash)
661
+ raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)
662
+ end
663
+
664
+ unless params[:protocolVersion].is_a?(String)
665
+ raise RequestHandlerError.new("Missing or invalid protocolVersion", params, error_type: :invalid_params)
666
+ end
667
+
668
+ unless params[:capabilities].is_a?(Hash)
669
+ raise RequestHandlerError.new("Missing or invalid capabilities", params, error_type: :invalid_params)
670
+ end
671
+
672
+ client_info = params[:clientInfo]
673
+ if !client_info.is_a?(Hash) || client_info[:name].nil? || client_info[:version].nil?
674
+ raise RequestHandlerError.new("Missing or invalid clientInfo", params, error_type: :invalid_params)
675
+ end
676
+ end
677
+
678
+ # Handles `server/discover` (MCP 2026-07-28 draft, SEP-2575): sessionless capability discovery.
679
+ # Unlike `init`, this is state-free and idempotent: it stores no client info, does not mark
680
+ # the session initialized, and responds regardless of capability declarations or initialization state,
681
+ # so clients can probe a server before (or instead of) `initialize`. `serverInfo` is returned unfiltered
682
+ # because discovery happens before version negotiation. The draft's `ttlMs`/`cacheScope` cache hints
683
+ # are not included here yet.
684
+ # https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
685
+ def discover(_request)
686
+ {
687
+ supportedVersions: Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS,
688
+ capabilities: capabilities,
689
+ serverInfo: server_info,
690
+ instructions: instructions,
691
+ }.compact
692
+ end
693
+
581
694
  def configure_logging_level(request, session: nil)
582
695
  if capabilities[:logging].nil?
583
696
  raise RequestHandlerError.new("Server does not support logging", request, error_type: :internal_error)
@@ -597,7 +710,7 @@ module MCP
597
710
  def list_tools(request)
598
711
  page = paginate(@tools.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
599
712
 
600
- { tools: page[:items], nextCursor: page[:next_cursor] }.compact
713
+ apply_cache_metadata({ tools: page[:items], nextCursor: page[:next_cursor] }.compact)
601
714
  end
602
715
 
603
716
  def call_tool(request, session: nil, related_request_id: nil, cancellation: nil)
@@ -653,7 +766,7 @@ module MCP
653
766
  def list_prompts(request)
654
767
  page = paginate(@prompts.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
655
768
 
656
- { prompts: page[:items], nextCursor: page[:next_cursor] }.compact
769
+ apply_cache_metadata({ prompts: page[:items], nextCursor: page[:next_cursor] }.compact)
657
770
  end
658
771
 
659
772
  def get_prompt(request, session: nil, related_request_id: nil, cancellation: nil)
@@ -682,19 +795,91 @@ module MCP
682
795
  def list_resources(request)
683
796
  page = paginate(@resources, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
684
797
 
685
- { resources: page[:items], nextCursor: page[:next_cursor] }.compact
798
+ apply_cache_metadata({ resources: page[:items], nextCursor: page[:next_cursor] }.compact)
686
799
  end
687
800
 
688
- # Server implementation should set `resources_read_handler` to override no-op default
689
- def read_resource_no_content(request)
690
- add_instrumentation_data(resource_uri: request[:uri])
691
- []
801
+ # Default `resources/read` handler: routes to class-based resources and resource templates.
802
+ # Fully replaced when `resources_read_handler` is set. When no class-based resource or template is registered,
803
+ # unknown URIs keep the historical no-op `[]` response instead of raising.
804
+ def read_resource(request, server_context: nil)
805
+ uri = request[:uri]
806
+ add_instrumentation_data(resource_uri: uri)
807
+
808
+ resource = @resource_index[uri]
809
+ if resource.is_a?(Class)
810
+ return normalize_resource_contents(call_resource_contents(resource, server_context))
811
+ end
812
+
813
+ @resource_templates.each do |template|
814
+ next unless template.is_a?(Class)
815
+
816
+ params = template.match_uri(uri)
817
+ next unless params
818
+
819
+ add_instrumentation_data(resource_template: template.uri_template)
820
+ return normalize_resource_contents(call_template_contents(template, params, server_context))
821
+ end
822
+
823
+ return [] unless class_based_resources_registered?
824
+
825
+ raise ResourceNotFoundError.new(uri, request)
826
+ end
827
+
828
+ def call_resource_contents(resource, server_context)
829
+ if accepts_server_context?(resource.method(:contents))
830
+ resource.contents(server_context: server_context)
831
+ else
832
+ resource.contents
833
+ end
834
+ end
835
+
836
+ def call_template_contents(template, params, server_context)
837
+ if accepts_server_context?(template.method(:contents))
838
+ template.contents(**params, server_context: server_context)
839
+ else
840
+ template.contents(**params)
841
+ end
842
+ end
843
+
844
+ def normalize_resource_contents(result)
845
+ contents = result.is_a?(Array) ? result : [result]
846
+ contents.map(&:to_h)
847
+ end
848
+
849
+ def class_based_resources_registered?
850
+ @resources.any? { |resource| resource.is_a?(Class) } ||
851
+ @resource_templates.any? { |template| template.is_a?(Class) }
692
852
  end
693
853
 
694
854
  def list_resource_templates(request)
695
855
  page = paginate(@resource_templates, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
696
856
 
697
- { resourceTemplates: page[:items], nextCursor: page[:next_cursor] }.compact
857
+ apply_cache_metadata({ resourceTemplates: page[:items], nextCursor: page[:next_cursor] }.compact)
858
+ end
859
+
860
+ # Builds the `resources/read` result. The documented handler contract is "return value becomes `contents`";
861
+ # a Hash with a `:contents` key is also accepted as a full result so a handler can override the server-level
862
+ # `ttlMs`/`cacheScope` cache hints per result (SEP-2549).
863
+ def build_read_resource_result(handler_result)
864
+ result = if handler_result.is_a?(Hash) && handler_result.key?(:contents)
865
+ handler_result
866
+ else
867
+ { contents: handler_result }
868
+ end
869
+
870
+ apply_cache_metadata(result)
871
+ end
872
+
873
+ # Adds the SEP-2549 cache hints (`ttlMs`, `cacheScope`) to a result. Emission is opt-in: nothing is added
874
+ # unless the server was configured with `ttl_ms`/`cache_scope` or the result already carries one of the fields, in
875
+ # which case the missing one is filled with the spec defaults (`ttlMs: 0` = do not cache, `cacheScope: "public"`).
876
+ # Values already in the result win, enabling per-result overrides.
877
+ # https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549
878
+ def apply_cache_metadata(result)
879
+ explicit = result.key?(:ttlMs) || result.key?(:cacheScope)
880
+ return result if @ttl_ms.nil? && @cache_scope.nil? && !explicit
881
+
882
+ { ttlMs: @ttl_ms || 0, cacheScope: @cache_scope || "public" }.merge(result)
698
883
  end
699
884
 
700
885
  def complete(params, session: nil, related_request_id: nil, cancellation: nil)
@@ -35,6 +35,9 @@ module MCP
35
35
  # @param data [Object] The log data to send.
36
36
  # @param level [String] Log level (e.g., `"debug"`, `"info"`, `"error"`).
37
37
  # @param logger [String, nil] Logger name.
38
+ # @deprecated MCP Logging (`logging/setLevel` and `notifications/message`)
39
+ # is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
40
+ # Use stderr or OpenTelemetry instead.
38
41
  def notify_log_message(data:, level:, logger: nil)
39
42
  return unless @notification_target
40
43
 
@@ -51,6 +54,13 @@ module MCP
51
54
  end
52
55
 
53
56
  # Delegates to the session so the request is scoped to the originating client.
57
+ # The originating request id is stamped as `related_request_id`, satisfying
58
+ # SEP-2260's requirement that server-to-client requests be associated with
59
+ # the client request being processed.
60
+ # @deprecated MCP Roots (`roots/list` and
61
+ # `notifications/roots/list_changed`) is deprecated as of MCP protocol
62
+ # version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
63
+ # server configuration, or environment variables instead.
54
64
  def list_roots
55
65
  if @notification_target.respond_to?(:list_roots)
56
66
  @notification_target.list_roots(related_request_id: @related_request_id)
@@ -84,6 +94,14 @@ module MCP
84
94
  # Delegates to the session so the request is scoped to the originating client.
85
95
  # Falls back to `@context` (via `method_missing`) when `@notification_target`
86
96
  # does not support sampling.
97
+ #
98
+ # The originating request id is stamped as `related_request_id` and cannot be
99
+ # overridden by callers (the literal keyword after `**kwargs` wins),
100
+ # satisfying SEP-2260's requirement that server-to-client requests be
101
+ # associated with the client request being processed.
102
+ # @deprecated MCP Sampling (`sampling/createMessage`) is deprecated as of
103
+ # MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider
104
+ # APIs instead.
87
105
  def create_sampling_message(**kwargs)
88
106
  if @notification_target.respond_to?(:create_sampling_message)
89
107
  @notification_target.create_sampling_message(**kwargs, related_request_id: @related_request_id)
@@ -97,6 +115,8 @@ module MCP
97
115
  # Delegates to the session so the request is scoped to the originating client.
98
116
  # Falls back to `@context` (via `method_missing`) when `@notification_target`
99
117
  # does not support elicitation.
118
+ # The originating request id is stamped as a non-overridable
119
+ # `related_request_id`, as with `create_sampling_message` (SEP-2260).
100
120
  def create_form_elicitation(**kwargs)
101
121
  if @notification_target.respond_to?(:create_form_elicitation)
102
122
  @notification_target.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
@@ -109,6 +129,8 @@ module MCP
109
129
 
110
130
  # Delegates to the session so the request is scoped to the originating client.
111
131
  # Falls back to `@context` when `@notification_target` does not support URL mode elicitation.
132
+ # The originating request id is stamped as a non-overridable `related_request_id`,
133
+ # as with `create_sampling_message` (SEP-2260).
112
134
  def create_url_elicitation(**kwargs)
113
135
  if @notification_target.respond_to?(:create_url_elicitation)
114
136
  @notification_target.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
@@ -98,7 +98,17 @@ module MCP
98
98
  end
99
99
 
100
100
  # Sends a `roots/list` request scoped to this session.
101
+ #
102
+ # Per SEP-2260, the request must be associated with an originating client
103
+ # request; prefer `server_context.list_roots` inside a handler, which stamps
104
+ # the association automatically.
105
+ # @deprecated MCP Roots (`roots/list` and
106
+ # `notifications/roots/list_changed`) is deprecated as of MCP protocol
107
+ # version 2026-07-28 (SEP-2577). Use tool parameters, resource URIs,
108
+ # server configuration, or environment variables instead.
101
109
  def list_roots(related_request_id: nil)
110
+ warn_unassociated_request(__method__, related_request_id)
111
+
102
112
  unless client_capabilities&.dig(:roots)
103
113
  raise "Client does not support roots."
104
114
  end
@@ -115,13 +125,28 @@ module MCP
115
125
  end
116
126
 
117
127
  # Sends a `sampling/createMessage` request scoped to this session.
128
+ #
129
+ # Per SEP-2260, the request must be associated with an originating client
130
+ # request; prefer `server_context.create_sampling_message` inside a handler,
131
+ # which stamps the association automatically.
132
+ # @deprecated MCP Sampling (`sampling/createMessage`) is deprecated as of
133
+ # MCP protocol version 2026-07-28 (SEP-2577). Use direct LLM provider
134
+ # APIs instead.
118
135
  def create_sampling_message(related_request_id: nil, **kwargs)
136
+ warn_unassociated_request(__method__, related_request_id)
137
+
119
138
  params = @server.build_sampling_params(client_capabilities, **kwargs)
120
139
  send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id)
121
140
  end
122
141
 
123
142
  # Sends an `elicitation/create` request (form mode) scoped to this session.
143
+ #
144
+ # Per SEP-2260, the request must be associated with an originating client
145
+ # request; prefer `server_context.create_form_elicitation` inside a handler,
146
+ # which stamps the association automatically.
124
147
  def create_form_elicitation(message:, requested_schema:, related_request_id: nil)
148
+ warn_unassociated_request(__method__, related_request_id)
149
+
125
150
  unless client_capabilities&.dig(:elicitation)
126
151
  raise "Client does not support elicitation. " \
127
152
  "The client must declare the `elicitation` capability during initialization."
@@ -132,7 +157,13 @@ module MCP
132
157
  end
133
158
 
134
159
  # Sends an `elicitation/create` request (URL mode) scoped to this session.
160
+ #
161
+ # Per SEP-2260, the request must be associated with an originating client
162
+ # request; prefer `server_context.create_url_elicitation` inside a handler,
163
+ # which stamps the association automatically.
135
164
  def create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil)
165
+ warn_unassociated_request(__method__, related_request_id)
166
+
136
167
  unless client_capabilities&.dig(:elicitation, :url)
137
168
  raise "Client does not support URL mode elicitation. " \
138
169
  "The client must declare the `elicitation.url` capability during initialization."
@@ -188,6 +219,9 @@ module MCP
188
219
  end
189
220
 
190
221
  # Sends a log message notification to this session only.
222
+ # @deprecated MCP Logging (`logging/setLevel` and `notifications/message`)
223
+ # is deprecated as of MCP protocol version 2026-07-28 (SEP-2577).
224
+ # Use stderr or OpenTelemetry instead.
191
225
  def notify_log_message(data:, level:, logger: nil, related_request_id: nil)
192
226
  effective_logging = @logging_message_notification || @server.logging_message_notification
193
227
  return unless effective_logging&.should_notify?(level)
@@ -254,5 +288,21 @@ module MCP
254
288
  # The explicit splat suppresses that conversion and is a no-op when `supported` is empty.
255
289
  transport_method.call(method, params, **supported)
256
290
  end
291
+
292
+ # Per SEP-2260, servers MUST send `roots/list`, `sampling/createMessage`, and `elicitation/create` only
293
+ # in association with an originating client request (`ping` is exempt). A request without `related_request_id:` is
294
+ # delivered on the standalone GET stream by the Streamable HTTP transport, which the 2026-07-28 spec forbids;
295
+ # warn so callers migrate to the `server_context` helpers before this becomes an error.
296
+ # https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2260
297
+ def warn_unassociated_request(method_name, related_request_id)
298
+ return if related_request_id
299
+
300
+ warn(
301
+ "Calling `ServerSession##{method_name}` without `related_request_id:` is deprecated (SEP-2260): " \
302
+ "server-to-client #{method_name} requests must be associated with an originating client request. " \
303
+ "Use `server_context.#{method_name}` inside a handler, or pass `related_request_id:` explicitly.",
304
+ uplevel: 2,
305
+ )
306
+ end
257
307
  end
258
308
  end
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "0.22.0"
4
+ VERSION = "0.24.0"
5
5
  end
data/lib/mcp.rb CHANGED
@@ -8,14 +8,17 @@ require_relative "mcp/version"
8
8
 
9
9
  module MCP
10
10
  autoload :Annotations, "mcp/annotations"
11
+ autoload :Apps, "mcp/apps"
11
12
  autoload :Cancellation, "mcp/cancellation"
12
13
  autoload :CancelledError, "mcp/cancelled_error"
13
14
  autoload :Client, "mcp/client"
14
15
  autoload :Content, "mcp/content"
16
+ autoload :ErrorCodes, "mcp/error_codes"
15
17
  autoload :Icon, "mcp/icon"
16
18
  autoload :Prompt, "mcp/prompt"
17
19
  autoload :Resource, "mcp/resource"
18
20
  autoload :ResourceTemplate, "mcp/resource_template"
21
+ autoload :ResultType, "mcp/result_type"
19
22
  autoload :Server, "mcp/server"
20
23
  autoload :ServerSession, "mcp/server_session"
21
24
  autoload :Tool, "mcp/tool"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -37,15 +37,18 @@ files:
37
37
  - lib/json_rpc_handler.rb
38
38
  - lib/mcp.rb
39
39
  - lib/mcp/annotations.rb
40
+ - lib/mcp/apps.rb
40
41
  - lib/mcp/cancellation.rb
41
42
  - lib/mcp/cancelled_error.rb
42
43
  - lib/mcp/client.rb
44
+ - lib/mcp/client/elicitation.rb
43
45
  - lib/mcp/client/http.rb
44
46
  - lib/mcp/client/oauth.rb
45
47
  - lib/mcp/client/oauth/client_credentials_provider.rb
46
48
  - lib/mcp/client/oauth/discovery.rb
47
49
  - lib/mcp/client/oauth/flow.rb
48
50
  - lib/mcp/client/oauth/in_memory_storage.rb
51
+ - lib/mcp/client/oauth/jwt_client_assertion.rb
49
52
  - lib/mcp/client/oauth/pkce.rb
50
53
  - lib/mcp/client/oauth/provider.rb
51
54
  - lib/mcp/client/oauth/storage_backed_provider.rb
@@ -54,6 +57,7 @@ files:
54
57
  - lib/mcp/client/tool.rb
55
58
  - lib/mcp/configuration.rb
56
59
  - lib/mcp/content.rb
60
+ - lib/mcp/error_codes.rb
57
61
  - lib/mcp/icon.rb
58
62
  - lib/mcp/instrumentation.rb
59
63
  - lib/mcp/logging_message_notification.rb
@@ -67,6 +71,7 @@ files:
67
71
  - lib/mcp/resource/contents.rb
68
72
  - lib/mcp/resource/embedded.rb
69
73
  - lib/mcp/resource_template.rb
74
+ - lib/mcp/result_type.rb
70
75
  - lib/mcp/server.rb
71
76
  - lib/mcp/server/capabilities.rb
72
77
  - lib/mcp/server/pagination.rb
@@ -90,7 +95,7 @@ licenses:
90
95
  - Apache-2.0
91
96
  metadata:
92
97
  allowed_push_host: https://rubygems.org
93
- changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.22.0
98
+ changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.24.0
94
99
  homepage_uri: https://ruby.sdk.modelcontextprotocol.io
95
100
  source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
96
101
  bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues