mcp 1.7.0 → 1.8.1

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
@@ -13,6 +13,7 @@ require_relative "protocol_deprecations"
13
13
  require_relative "server_context"
14
14
  require_relative "server/capabilities"
15
15
  require_relative "server/input_required_result"
16
+ require_relative "server/oauth"
16
17
  require_relative "server/pagination"
17
18
  require_relative "server/pending_response"
18
19
  require_relative "server/request_state_security"
@@ -270,6 +271,9 @@ module MCP
270
271
  Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
271
272
  Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
272
273
  }
274
+ # The protocol's own request methods, as opposed to the notifications above and to custom methods
275
+ # registered later: these need a request id to be run.
276
+ @protocol_request_methods = @handlers.keys.reject { |name| Methods.notification?(name) }.freeze
273
277
  @transport = transport
274
278
  end
275
279
 
@@ -279,10 +283,13 @@ module MCP
279
283
  # @param session [ServerSession, nil] Per-connection session. Passed by
280
284
  # `ServerSession#handle` for session-scoped notification delivery.
281
285
  # When `nil`, progress and logging notifications from tool handlers are silently skipped.
286
+ # @param auth_info [Server::OAuth::AccessToken, nil] The verified access token
287
+ # for the current request, passed by the transport layer and exposed to
288
+ # handlers as `server_context.auth_info`.
282
289
  # @return [Hash, nil] The JSON-RPC response, or `nil` for notifications.
283
- def handle(request, session: nil)
284
- JsonRpcHandler.handle(request) do |method, request_id|
285
- handle_request(request, method, session: session, related_request_id: request_id)
290
+ def handle(request, session: nil, auth_info: nil)
291
+ JsonRpcHandler.handle(request, id_validation_pattern: nil) do |method, request_id|
292
+ handle_request(request, method, session: session, related_request_id: request_id, auth_info: auth_info)
286
293
  end
287
294
  end
288
295
 
@@ -292,10 +299,12 @@ module MCP
292
299
  # @param session [ServerSession, nil] Per-connection session. Passed by
293
300
  # `ServerSession#handle_json` for session-scoped notification delivery.
294
301
  # When `nil`, progress and logging notifications from tool handlers are silently skipped.
302
+ # @param auth_info [Server::OAuth::AccessToken, nil] The verified access token
303
+ # for the current request, as in `handle`.
295
304
  # @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
296
- def handle_json(request, session: nil)
297
- JsonRpcHandler.handle_json(request) do |method, request_id|
298
- handle_request(request, method, session: session, related_request_id: request_id)
305
+ def handle_json(request, session: nil, auth_info: nil)
306
+ JsonRpcHandler.handle_json(request, id_validation_pattern: nil) do |method, request_id|
307
+ handle_request(request, method, session: session, related_request_id: request_id, auth_info: auth_info)
299
308
  end
300
309
  end
301
310
 
@@ -600,7 +609,7 @@ module MCP
600
609
  end
601
610
  end
602
611
 
603
- def handle_request(request, method, session: nil, related_request_id: nil)
612
+ def handle_request(request, method, session: nil, related_request_id: nil, auth_info: nil)
604
613
  # A well-formed notification carries no JSON-RPC id and receives no response.
605
614
  # If a client erroneously sends a notification-only method with an id, the message
606
615
  # is framed as a request; since notification methods have no request handler,
@@ -608,6 +617,12 @@ module MCP
608
617
  # the TypeScript and Python SDKs rather than emitting a spurious `result: null`.
609
618
  return if Methods.notification?(method) && !related_request_id.nil?
610
619
 
620
+ # The mirror case: a request method of the protocol sent without an id is framed as a notification,
621
+ # whose result the sender can never receive, so it is not run either; the TypeScript and Python servers
622
+ # drop such a message the same way, as a notification they have no handler for. A custom method keeps
623
+ # running without an id, since `define_custom_method` serves notifications too.
624
+ return if related_request_id.nil? && @protocol_request_methods.include?(method)
625
+
611
626
  # `notifications/cancelled` is dispatched directly: it is a notification (no JSON-RPC id)
612
627
  # and intentionally bypasses the `@handlers` lookup, capability check, in-flight registry,
613
628
  # and rescue blocks below.
@@ -639,10 +654,18 @@ module MCP
639
654
  begin
640
655
  Methods.ensure_capability!(method, capabilities)
641
656
  rescue Methods::MissingRequiredCapabilityError => e
642
- # Re-raise through RequestHandlerError, the one channel whose message is
643
- # deliberately surfaced to clients: `JsonRpcHandler`'s blind `rescue StandardError`
644
- # no longer echoes exception messages into the error `data` member (CWE-209).
645
- raise RequestHandlerError.new(e.message, request, error_type: :internal_error, original_error: e)
657
+ # A method of a capability the server does not declare is one the server does not implement, so it is answered as Method not found,
658
+ # which the TypeScript and Python servers return for a handler they never registered, and which the Streamable HTTP transport maps to
659
+ # 404 on the modern lifecycle. Re-raised through RequestHandlerError, the one channel whose message is deliberately surfaced to clients:
660
+ # `JsonRpcHandler`'s blind `rescue StandardError` no longer echoes exception messages into the error `data` member (CWE-209).
661
+ raise RequestHandlerError.new(
662
+ "Method not found",
663
+ request,
664
+ error_type: :method_not_found,
665
+ error_code: JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND,
666
+ error_data: e.message,
667
+ original_error: e,
668
+ )
646
669
  end
647
670
 
648
671
  # `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2),
@@ -691,25 +714,25 @@ module MCP
691
714
  when Methods::INITIALIZE
692
715
  init(params, session: session)
693
716
  when Methods::RESOURCES_READ
694
- contents = read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
717
+ contents = read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
695
718
 
696
719
  # An SEP-2322 `input_required` result must not be wrapped as `contents` or stamped with SEP-2549 cache hints.
697
720
  contents.is_a?(InputRequiredResult) ? contents : build_read_resource_result(contents)
698
721
  when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
699
722
  validate_resource_subscription_params!(params)
700
- handler_result = dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
723
+ handler_result = dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
701
724
 
702
725
  subscription_result(handler_result)
703
726
  when Methods::TOOLS_CALL
704
- call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
727
+ call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
705
728
  when Methods::PROMPTS_GET
706
- get_prompt(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
729
+ get_prompt(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
707
730
  when Methods::COMPLETION_COMPLETE
708
- complete(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
731
+ complete(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
709
732
  when Methods::LOGGING_SET_LEVEL
710
733
  configure_logging_level(params, session: session)
711
734
  else
712
- dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
735
+ dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
713
736
  end
714
737
  client = session&.client || @client
715
738
  add_instrumentation_data(client: client) if client
@@ -760,6 +783,15 @@ module MCP
760
783
  rescue CancelledError => e
761
784
  add_instrumentation_data(cancelled: true, cancellation_reason: e.reason)
762
785
  next JsonRpcHandler::NO_RESPONSE
786
+ rescue OAuth::InsufficientScopeError => e
787
+ # A handler's `require_scopes!` failure is a client authorization error, not a server fault:
788
+ # surface it as an invalid-request JSON-RPC error whose message names the missing scopes.
789
+ # HTTP-level 403 challenges remain the transport `required_scopes` gate's job.
790
+ report_exception(e, { request: request })
791
+ add_instrumentation_data(error: :insufficient_scope)
792
+ converted = RequestHandlerError.new(e.message, request, error_type: :invalid_request, original_error: e)
793
+ reported_exception = converted
794
+ raise converted
763
795
  rescue RequestHandlerError => e
764
796
  report_exception(e.original_error || e, { request: request })
765
797
  add_instrumentation_data(error: e.error_type)
@@ -1116,7 +1148,7 @@ module MCP
1116
1148
 
1117
1149
  {
1118
1150
  protocolVersion: negotiated_version,
1119
- capabilities: capabilities,
1151
+ capabilities: handshake_capabilities,
1120
1152
  serverInfo: info,
1121
1153
  instructions: response_instructions,
1122
1154
  }.compact
@@ -1193,6 +1225,19 @@ module MCP
1193
1225
  def discover_capabilities
1194
1226
  return capabilities if @transport.respond_to?(:serves_subscriptions_listen?) && @transport.serves_subscriptions_listen?
1195
1227
 
1228
+ without_notification_flags(capabilities)
1229
+ end
1230
+
1231
+ # Capabilities as advertised by `initialize`. A transport that delivers no notifications to its sessions
1232
+ # (stateless Streamable HTTP) cannot keep the promise the `listChanged` and `subscribe` flags make, so they are
1233
+ # left out, as `server/discover` leaves them out when `subscriptions/listen` is not served.
1234
+ def handshake_capabilities
1235
+ return capabilities unless @transport.respond_to?(:delivers_notifications?) && !@transport.delivers_notifications?
1236
+
1237
+ without_notification_flags(capabilities)
1238
+ end
1239
+
1240
+ def without_notification_flags(capabilities)
1196
1241
  capabilities.each_with_object({}) do |(name, value), stripped|
1197
1242
  stripped[name] = if value.is_a?(Hash)
1198
1243
  value.reject { |flag, _| ["listChanged", "subscribe"].include?(flag.to_s) }
@@ -1224,7 +1269,7 @@ module MCP
1224
1269
  apply_cache_metadata({ tools: page[:items], nextCursor: page[:next_cursor] }.compact)
1225
1270
  end
1226
1271
 
1227
- def call_tool(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
1272
+ def call_tool(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
1228
1273
  tool_name = request[:name]
1229
1274
 
1230
1275
  tool = tools[tool_name]
@@ -1259,12 +1304,13 @@ module MCP
1259
1304
  response = call_tool_with_args(
1260
1305
  tool,
1261
1306
  arguments,
1262
- server_context_with_meta(request),
1307
+ server_context_with_meta(request, auth_info: auth_info),
1263
1308
  progress_token: progress_token,
1264
1309
  session: session,
1265
1310
  related_request_id: related_request_id,
1266
1311
  cancellation: cancellation,
1267
1312
  envelope: envelope,
1313
+ auth_info: auth_info,
1268
1314
  retry_fields: mrtr_retry_fields(request),
1269
1315
  )
1270
1316
  # An SEP-2322 `input_required` result is not a tool result: output schema
@@ -1278,9 +1324,11 @@ module MCP
1278
1324
  result,
1279
1325
  content_provided: response.respond_to?(:content_provided?) && response.content_provided?,
1280
1326
  )
1281
- rescue RequestHandlerError, CancelledError
1327
+ rescue RequestHandlerError, CancelledError, OAuth::InsufficientScopeError
1282
1328
  # CancelledError is intentionally not wrapped so `handle_request` can turn it into
1283
- # `JsonRpcHandler::NO_RESPONSE` per the MCP cancellation spec.
1329
+ # `JsonRpcHandler::NO_RESPONSE` per the MCP cancellation spec, and
1330
+ # `OAuth::InsufficientScopeError` so `require_scopes!` failures keep their
1331
+ # scope-naming message instead of the generic internal-error shape.
1284
1332
  raise
1285
1333
  rescue => e
1286
1334
  # `e.message` is deliberately not included: it can carry internals (class, method
@@ -1300,7 +1348,7 @@ module MCP
1300
1348
  apply_cache_metadata({ prompts: page[:items], nextCursor: page[:next_cursor] }.compact)
1301
1349
  end
1302
1350
 
1303
- def get_prompt(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
1351
+ def get_prompt(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
1304
1352
  prompt_name = request[:name]
1305
1353
  prompt = @prompts[prompt_name]
1306
1354
  unless prompt
@@ -1328,6 +1376,7 @@ module MCP
1328
1376
  related_request_id: related_request_id,
1329
1377
  cancellation: cancellation,
1330
1378
  envelope: envelope,
1379
+ auth_info: auth_info,
1331
1380
  )
1332
1381
 
1333
1382
  call_prompt_template_with_args(prompt, prompt_args, server_context)
@@ -1440,7 +1489,7 @@ module MCP
1440
1489
  { ttlMs: @ttl_ms || 0, cacheScope: @cache_scope || "private" }.merge(result)
1441
1490
  end
1442
1491
 
1443
- def complete(params, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
1492
+ def complete(params, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
1444
1493
  validate_completion_params!(params)
1445
1494
 
1446
1495
  result = dispatch_optional_context_handler(
@@ -1450,6 +1499,7 @@ module MCP
1450
1499
  related_request_id: related_request_id,
1451
1500
  cancellation: cancellation,
1452
1501
  envelope: envelope,
1502
+ auth_info: auth_info,
1453
1503
  )
1454
1504
 
1455
1505
  normalize_completion_result(result)
@@ -1458,7 +1508,7 @@ module MCP
1458
1508
  # Invokes `resources/read` via the registered handler. If the handler block opts in to `server_context:`,
1459
1509
  # pass an `MCP::ServerContext` so the handler can observe cancellation via `server_context.cancelled?` or
1460
1510
  # `server_context.raise_if_cancelled!`.
1461
- def read_resource_contents(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
1511
+ def read_resource_contents(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
1462
1512
  dispatch_optional_context_handler(
1463
1513
  @handlers[Methods::RESOURCES_READ],
1464
1514
  request,
@@ -1466,6 +1516,7 @@ module MCP
1466
1516
  related_request_id: related_request_id,
1467
1517
  cancellation: cancellation,
1468
1518
  envelope: envelope,
1519
+ auth_info: auth_info,
1469
1520
  )
1470
1521
  end
1471
1522
 
@@ -1473,7 +1524,7 @@ module MCP
1473
1524
  # `completion_handler`, `resources_subscribe_handler`, `resources_unsubscribe_handler`, or `define_custom_method`.
1474
1525
  # Existing handlers that only accept `params` are called unchanged; handlers that declare a `server_context:`
1475
1526
  # keyword receive an `MCP::ServerContext` wrapping the raw server context with cancellation plumbing.
1476
- def dispatch_optional_context_handler(handler, params, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
1527
+ def dispatch_optional_context_handler(handler, params, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
1477
1528
  return handler.call(params) unless handler_declares_server_context?(handler)
1478
1529
 
1479
1530
  server_context = build_server_context(
@@ -1482,6 +1533,7 @@ module MCP
1482
1533
  related_request_id: related_request_id,
1483
1534
  cancellation: cancellation,
1484
1535
  envelope: envelope,
1536
+ auth_info: auth_info,
1485
1537
  )
1486
1538
  handler.call(params, server_context: server_context)
1487
1539
  end
@@ -1506,13 +1558,13 @@ module MCP
1506
1558
 
1507
1559
  # Builds an `MCP::ServerContext` used to give a handler access to session-scoped helpers
1508
1560
  # (progress, cancellation, nested server-to-client requests).
1509
- def build_server_context(request:, session:, related_request_id:, cancellation:, envelope: nil)
1561
+ def build_server_context(request:, session:, related_request_id:, cancellation:, envelope: nil, auth_info: nil)
1510
1562
  meta_source = request.is_a?(Hash) ? request : {}
1511
1563
  progress_token = meta_source.dig(:_meta, :progressToken)
1512
1564
  progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
1513
1565
  retry_fields = mrtr_retry_fields(meta_source)
1514
1566
  ServerContext.new(
1515
- server_context_with_meta(meta_source),
1567
+ server_context_with_meta(meta_source, auth_info: auth_info),
1516
1568
  progress: progress,
1517
1569
  notification_target: session,
1518
1570
  related_request_id: related_request_id,
@@ -1520,6 +1572,7 @@ module MCP
1520
1572
  envelope: envelope,
1521
1573
  input_responses: retry_fields[:input_responses],
1522
1574
  request_state: retry_fields[:request_state],
1575
+ auth_info: auth_info,
1523
1576
  )
1524
1577
  end
1525
1578
 
@@ -1579,7 +1632,7 @@ module MCP
1579
1632
  end
1580
1633
  end
1581
1634
 
1582
- def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, retry_fields: nil)
1635
+ def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil, retry_fields: nil)
1583
1636
  # Transports parse incoming JSON with `symbolize_names: true`, so `arguments` already arrives symbolized
1584
1637
  # at every nesting level. This top-level transform only guards callers that hand in string-keyed top-level arguments;
1585
1638
  # it does not recurse, and nested object keys remain symbols. Tools therefore receive symbol keys all the way down.
@@ -1597,6 +1650,7 @@ module MCP
1597
1650
  envelope: envelope,
1598
1651
  input_responses: retry_fields&.fetch(:input_responses, nil),
1599
1652
  request_state: retry_fields&.fetch(:request_state, nil),
1653
+ auth_info: auth_info,
1600
1654
  )
1601
1655
  tool.call(**args, server_context: server_context)
1602
1656
  else
@@ -1614,15 +1668,18 @@ module MCP
1614
1668
  raw_result.is_a?(InputRequiredResult) ? raw_result : raw_result.to_h
1615
1669
  end
1616
1670
 
1617
- def server_context_with_meta(request)
1671
+ def server_context_with_meta(request, auth_info: nil)
1618
1672
  meta = request[:_meta]
1619
- if meta && server_context.is_a?(Hash)
1620
- context = server_context.dup
1621
- context[:_meta] = meta
1673
+ return server_context if meta.nil? && auth_info.nil?
1674
+
1675
+ if server_context.is_a?(Hash) || server_context.nil?
1676
+ context = (server_context || {}).dup
1677
+ context[:_meta] = meta if meta
1678
+ context[:auth_info] = auth_info if auth_info
1622
1679
  context
1623
- elsif meta && server_context.nil?
1624
- { _meta: meta }
1625
1680
  else
1681
+ # A custom (non-Hash) user context is passed through untouched; handlers on that path read
1682
+ # the token via `server_context.auth_info` instead.
1626
1683
  server_context
1627
1684
  end
1628
1685
  end
@@ -15,8 +15,11 @@ module MCP
15
15
  # (the same access model as the envelope readers).
16
16
  attr_reader :input_responses, :request_state
17
17
 
18
- def initialize(context, progress:, notification_target:, related_request_id: nil, cancellation: nil, envelope: nil,
19
- input_responses: nil, request_state: nil)
18
+ # The verified `MCP::Server::OAuth::AccessToken` for the current request, or nil when the request was not bearer-authenticated.
19
+ # This reader is the canonical accessor; when the underlying context is a Hash the same value is also reachable as `server_context[:auth_info]`.
20
+ attr_reader :auth_info
21
+
22
+ def initialize(context, progress:, notification_target:, related_request_id: nil, cancellation: nil, envelope: nil, input_responses: nil, request_state: nil, auth_info: nil)
20
23
  @context = context
21
24
  @progress = progress
22
25
  @notification_target = notification_target
@@ -25,6 +28,7 @@ module MCP
25
28
  @envelope = envelope
26
29
  @input_responses = input_responses
27
30
  @request_state = request_state
31
+ @auth_info = auth_info
28
32
  end
29
33
 
30
34
  # Reads one entry of {#input_responses} by its `inputRequests` key, tolerating symbol or string keys.
@@ -89,6 +93,26 @@ module MCP
89
93
  raise Server::MissingRequiredClientCapabilityError, required
90
94
  end
91
95
 
96
+ # Whether the current request was bearer-authenticated.
97
+ def authenticated?
98
+ !@auth_info.nil?
99
+ end
100
+
101
+ # Guards the current operation on OAuth scopes the token must carry, for authorization decisions finer-grained than
102
+ # the transport-wide `required_scopes`. Raises `Server::OAuth::InsufficientScopeError`, which surfaces as
103
+ # a JSON-RPC invalid-request error naming the missing scopes. HTTP-level 403 step-up challenges remain the job of
104
+ # the transport's `required_scopes` gate, matching how the Python and TypeScript SDKs split endpoint-level challenges
105
+ # from in-handler authorization. The transport's `scope_matcher:` applies here as well: the authenticator attaches it
106
+ # to the token it hands over, so a scope hierarchy is judged the same way at the gate and in handlers.
107
+ def require_scopes!(*scopes)
108
+ raise ArgumentError, "at least one scope is required" if scopes.empty?
109
+
110
+ missing_scopes = scopes.reject { |scope| @auth_info&.scope?(scope) }
111
+ return if missing_scopes.empty?
112
+
113
+ raise Server::OAuth::InsufficientScopeError.new("Token is missing required scopes: #{missing_scopes.join(", ")}", required_scopes: scopes)
114
+ end
115
+
92
116
  # Reports progress for the current tool operation.
93
117
  # The notification is automatically scoped to the originating session.
94
118
  #
@@ -120,12 +120,22 @@ module MCP
120
120
  MCP.configuration.exception_reporter.call(e, { notification: "cancelled", request_id: request_id })
121
121
  end
122
122
 
123
- def handle(request)
124
- @server.handle(request, session: self)
123
+ # Accepts the request either as a positional Hash or as bare keyword arguments (`session.handle(jsonrpc: "2.0", ...)`),
124
+ # a calling style that predates this method having keyword parameters. `auth_info:` is honored only alongside a positional request:
125
+ # in the bare-keyword style every keyword belongs to the request body, and the body is attacker-authored JSON,
126
+ # so a captured `auth_info` keyword is folded back into the request rather than trusted as a verified credential.
127
+ def handle(request = nil, auth_info: nil, **request_keywords)
128
+ if request.nil?
129
+ request = request_keywords
130
+ request[:auth_info] = auth_info unless auth_info.nil?
131
+ auth_info = nil
132
+ end
133
+
134
+ @server.handle(request, session: self, auth_info: auth_info)
125
135
  end
126
136
 
127
- def handle_json(request_json)
128
- @server.handle_json(request_json, session: self)
137
+ def handle_json(request_json, auth_info: nil)
138
+ @server.handle_json(request_json, session: self, auth_info: auth_info)
129
139
  end
130
140
 
131
141
  # Called by `Server#init` during the initialization handshake.
data/lib/mcp/transport.rb CHANGED
@@ -57,6 +57,14 @@ module MCP
57
57
  false
58
58
  end
59
59
 
60
+ # Whether the transport delivers notifications to a connected client outside the response to one of its
61
+ # requests (`notifications/*/list_changed`, `notifications/resources/updated`). `Server#init` strips the
62
+ # `listChanged`/`subscribe` capability flags when it does not, since the handshake would promise notifications
63
+ # the transport drops.
64
+ def delivers_notifications?
65
+ true
66
+ end
67
+
60
68
  private
61
69
 
62
70
  def generate_request_id
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "1.7.0"
4
+ VERSION = "1.8.1"
5
5
  end
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: 1.7.0
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -85,6 +85,17 @@ files:
85
85
  - lib/mcp/server.rb
86
86
  - lib/mcp/server/capabilities.rb
87
87
  - lib/mcp/server/input_required_result.rb
88
+ - lib/mcp/server/oauth.rb
89
+ - lib/mcp/server/oauth/access_token.rb
90
+ - lib/mcp/server/oauth/authenticator.rb
91
+ - lib/mcp/server/oauth/challenge.rb
92
+ - lib/mcp/server/oauth/errors.rb
93
+ - lib/mcp/server/oauth/introspection_verifier.rb
94
+ - lib/mcp/server/oauth/jwt_verifier.rb
95
+ - lib/mcp/server/oauth/middleware.rb
96
+ - lib/mcp/server/oauth/protected_resource_metadata.rb
97
+ - lib/mcp/server/oauth/protected_resource_metadata_middleware.rb
98
+ - lib/mcp/server/oauth/token_verifier.rb
88
99
  - lib/mcp/server/pagination.rb
89
100
  - lib/mcp/server/pending_response.rb
90
101
  - lib/mcp/server/request_state_security.rb
@@ -108,7 +119,7 @@ licenses:
108
119
  - Apache-2.0
109
120
  metadata:
110
121
  allowed_push_host: https://rubygems.org
111
- changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.7.0
122
+ changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.8.1
112
123
  homepage_uri: https://ruby.sdk.modelcontextprotocol.io
113
124
  source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
114
125
  bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues