mcp 1.7.0 → 1.8.2
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/json_rpc_handler.rb +69 -20
- data/lib/mcp/client/http.rb +117 -25
- data/lib/mcp/client/oauth/flow.rb +68 -21
- data/lib/mcp/client/oauth/provider.rb +11 -0
- data/lib/mcp/client/stdio.rb +35 -6
- data/lib/mcp/server/oauth/access_token.rb +89 -0
- data/lib/mcp/server/oauth/authenticator.rb +150 -0
- data/lib/mcp/server/oauth/challenge.rb +114 -0
- data/lib/mcp/server/oauth/errors.rb +58 -0
- data/lib/mcp/server/oauth/introspection_verifier.rb +151 -0
- data/lib/mcp/server/oauth/jwt_verifier.rb +330 -0
- data/lib/mcp/server/oauth/middleware.rb +60 -0
- data/lib/mcp/server/oauth/protected_resource_metadata.rb +138 -0
- data/lib/mcp/server/oauth/protected_resource_metadata_middleware.rb +86 -0
- data/lib/mcp/server/oauth/token_verifier.rb +135 -0
- data/lib/mcp/server/oauth.rb +29 -0
- data/lib/mcp/server/transports/stdio_transport.rb +45 -8
- data/lib/mcp/server/transports/streamable_http_transport.rb +546 -149
- data/lib/mcp/server.rb +123 -35
- data/lib/mcp/server_context.rb +26 -2
- data/lib/mcp/server_session.rb +18 -8
- data/lib/mcp/transport.rb +8 -0
- data/lib/mcp/version.rb +1 -1
- metadata +13 -2
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,13 @@ 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
|
-
|
|
298
|
-
|
|
305
|
+
def handle_json(request, session: nil, auth_info: nil)
|
|
306
|
+
on_generate_error = ->(error) { report_exception(error, { error: "Failed to generate response" }) }
|
|
307
|
+
JsonRpcHandler.handle_json(request, id_validation_pattern: nil, on_generate_error: on_generate_error) do |method, request_id|
|
|
308
|
+
handle_request(request, method, session: session, related_request_id: request_id, auth_info: auth_info)
|
|
299
309
|
end
|
|
300
310
|
end
|
|
301
311
|
|
|
@@ -491,6 +501,36 @@ module MCP
|
|
|
491
501
|
@handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
|
|
492
502
|
end
|
|
493
503
|
|
|
504
|
+
# Returns the distinct members of `uris` that the `resources_subscribe_handler` accepts, calling it once per URI
|
|
505
|
+
# with `{ uri: }`, plus `_meta` when given. `subscriptions/listen` takes the place of `resources/subscribe`
|
|
506
|
+
# in the 2026-07-28 lifecycle, so the handler keeps deciding which resources a client may follow there too.
|
|
507
|
+
# A URI the handler raises for is reported and left out, as the spec lets the acknowledgement honor a subset of
|
|
508
|
+
# what was requested; any return value accepts the URI and is otherwise discarded. The `server_context` has no session,
|
|
509
|
+
# related request id, or cancellation. Public only so that `StreamableHTTPTransport` can call it.
|
|
510
|
+
#
|
|
511
|
+
# @api private
|
|
512
|
+
# @param uris [Array<String>] The `resourceSubscriptions` of a `subscriptions/listen` request.
|
|
513
|
+
# @param meta [Hash, nil] The `_meta` of that request.
|
|
514
|
+
# @param envelope [MCP::RequestEnvelope, nil] The envelope of that request.
|
|
515
|
+
# @param auth_info [MCP::Server::OAuth::AccessToken, nil] The token that authenticated that request.
|
|
516
|
+
# @return [Array<String>]
|
|
517
|
+
def subscribable_resource_uris(uris, meta: nil, envelope: nil, auth_info: nil)
|
|
518
|
+
handler = @handlers[Methods::RESOURCES_SUBSCRIBE]
|
|
519
|
+
|
|
520
|
+
uris.uniq.select do |uri|
|
|
521
|
+
params = { uri: uri }
|
|
522
|
+
params[:_meta] = meta if meta
|
|
523
|
+
dispatch_optional_context_handler(handler, params, envelope: envelope, auth_info: auth_info)
|
|
524
|
+
true
|
|
525
|
+
rescue RequestHandlerError => e
|
|
526
|
+
report_exception(e.original_error || e, { request: params })
|
|
527
|
+
false
|
|
528
|
+
rescue => e
|
|
529
|
+
report_exception(e, { request: params })
|
|
530
|
+
false
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
494
534
|
def build_sampling_params(
|
|
495
535
|
capabilities,
|
|
496
536
|
messages:,
|
|
@@ -600,7 +640,7 @@ module MCP
|
|
|
600
640
|
end
|
|
601
641
|
end
|
|
602
642
|
|
|
603
|
-
def handle_request(request, method, session: nil, related_request_id: nil)
|
|
643
|
+
def handle_request(request, method, session: nil, related_request_id: nil, auth_info: nil)
|
|
604
644
|
# A well-formed notification carries no JSON-RPC id and receives no response.
|
|
605
645
|
# If a client erroneously sends a notification-only method with an id, the message
|
|
606
646
|
# is framed as a request; since notification methods have no request handler,
|
|
@@ -608,6 +648,12 @@ module MCP
|
|
|
608
648
|
# the TypeScript and Python SDKs rather than emitting a spurious `result: null`.
|
|
609
649
|
return if Methods.notification?(method) && !related_request_id.nil?
|
|
610
650
|
|
|
651
|
+
# The mirror case: a request method of the protocol sent without an id is framed as a notification,
|
|
652
|
+
# whose result the sender can never receive, so it is not run either; the TypeScript and Python servers
|
|
653
|
+
# drop such a message the same way, as a notification they have no handler for. A custom method keeps
|
|
654
|
+
# running without an id, since `define_custom_method` serves notifications too.
|
|
655
|
+
return if related_request_id.nil? && @protocol_request_methods.include?(method)
|
|
656
|
+
|
|
611
657
|
# `notifications/cancelled` is dispatched directly: it is a notification (no JSON-RPC id)
|
|
612
658
|
# and intentionally bypasses the `@handlers` lookup, capability check, in-flight registry,
|
|
613
659
|
# and rescue blocks below.
|
|
@@ -639,10 +685,18 @@ module MCP
|
|
|
639
685
|
begin
|
|
640
686
|
Methods.ensure_capability!(method, capabilities)
|
|
641
687
|
rescue Methods::MissingRequiredCapabilityError => e
|
|
642
|
-
#
|
|
643
|
-
#
|
|
644
|
-
#
|
|
645
|
-
|
|
688
|
+
# 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,
|
|
689
|
+
# which the TypeScript and Python servers return for a handler they never registered, and which the Streamable HTTP transport maps to
|
|
690
|
+
# 404 on the modern lifecycle. Re-raised through RequestHandlerError, the one channel whose message is deliberately surfaced to clients:
|
|
691
|
+
# `JsonRpcHandler`'s blind `rescue StandardError` no longer echoes exception messages into the error `data` member (CWE-209).
|
|
692
|
+
raise RequestHandlerError.new(
|
|
693
|
+
"Method not found",
|
|
694
|
+
request,
|
|
695
|
+
error_type: :method_not_found,
|
|
696
|
+
error_code: JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND,
|
|
697
|
+
error_data: e.message,
|
|
698
|
+
original_error: e,
|
|
699
|
+
)
|
|
646
700
|
end
|
|
647
701
|
|
|
648
702
|
# `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2),
|
|
@@ -691,25 +745,25 @@ module MCP
|
|
|
691
745
|
when Methods::INITIALIZE
|
|
692
746
|
init(params, session: session)
|
|
693
747
|
when Methods::RESOURCES_READ
|
|
694
|
-
contents = read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
748
|
+
contents = read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
|
|
695
749
|
|
|
696
750
|
# An SEP-2322 `input_required` result must not be wrapped as `contents` or stamped with SEP-2549 cache hints.
|
|
697
751
|
contents.is_a?(InputRequiredResult) ? contents : build_read_resource_result(contents)
|
|
698
752
|
when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
|
|
699
753
|
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)
|
|
754
|
+
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
755
|
|
|
702
756
|
subscription_result(handler_result)
|
|
703
757
|
when Methods::TOOLS_CALL
|
|
704
|
-
call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
758
|
+
call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
|
|
705
759
|
when Methods::PROMPTS_GET
|
|
706
|
-
get_prompt(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
760
|
+
get_prompt(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
|
|
707
761
|
when Methods::COMPLETION_COMPLETE
|
|
708
|
-
complete(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
762
|
+
complete(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
|
|
709
763
|
when Methods::LOGGING_SET_LEVEL
|
|
710
764
|
configure_logging_level(params, session: session)
|
|
711
765
|
else
|
|
712
|
-
dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
766
|
+
dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope, auth_info: auth_info)
|
|
713
767
|
end
|
|
714
768
|
client = session&.client || @client
|
|
715
769
|
add_instrumentation_data(client: client) if client
|
|
@@ -760,6 +814,15 @@ module MCP
|
|
|
760
814
|
rescue CancelledError => e
|
|
761
815
|
add_instrumentation_data(cancelled: true, cancellation_reason: e.reason)
|
|
762
816
|
next JsonRpcHandler::NO_RESPONSE
|
|
817
|
+
rescue OAuth::InsufficientScopeError => e
|
|
818
|
+
# A handler's `require_scopes!` failure is a client authorization error, not a server fault:
|
|
819
|
+
# surface it as an invalid-request JSON-RPC error whose message names the missing scopes.
|
|
820
|
+
# HTTP-level 403 challenges remain the transport `required_scopes` gate's job.
|
|
821
|
+
report_exception(e, { request: request })
|
|
822
|
+
add_instrumentation_data(error: :insufficient_scope)
|
|
823
|
+
converted = RequestHandlerError.new(e.message, request, error_type: :invalid_request, original_error: e)
|
|
824
|
+
reported_exception = converted
|
|
825
|
+
raise converted
|
|
763
826
|
rescue RequestHandlerError => e
|
|
764
827
|
report_exception(e.original_error || e, { request: request })
|
|
765
828
|
add_instrumentation_data(error: e.error_type)
|
|
@@ -1116,7 +1179,7 @@ module MCP
|
|
|
1116
1179
|
|
|
1117
1180
|
{
|
|
1118
1181
|
protocolVersion: negotiated_version,
|
|
1119
|
-
capabilities:
|
|
1182
|
+
capabilities: handshake_capabilities,
|
|
1120
1183
|
serverInfo: info,
|
|
1121
1184
|
instructions: response_instructions,
|
|
1122
1185
|
}.compact
|
|
@@ -1193,6 +1256,19 @@ module MCP
|
|
|
1193
1256
|
def discover_capabilities
|
|
1194
1257
|
return capabilities if @transport.respond_to?(:serves_subscriptions_listen?) && @transport.serves_subscriptions_listen?
|
|
1195
1258
|
|
|
1259
|
+
without_notification_flags(capabilities)
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
# Capabilities as advertised by `initialize`. A transport that delivers no notifications to its sessions
|
|
1263
|
+
# (stateless Streamable HTTP) cannot keep the promise the `listChanged` and `subscribe` flags make, so they are
|
|
1264
|
+
# left out, as `server/discover` leaves them out when `subscriptions/listen` is not served.
|
|
1265
|
+
def handshake_capabilities
|
|
1266
|
+
return capabilities unless @transport.respond_to?(:delivers_notifications?) && !@transport.delivers_notifications?
|
|
1267
|
+
|
|
1268
|
+
without_notification_flags(capabilities)
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1271
|
+
def without_notification_flags(capabilities)
|
|
1196
1272
|
capabilities.each_with_object({}) do |(name, value), stripped|
|
|
1197
1273
|
stripped[name] = if value.is_a?(Hash)
|
|
1198
1274
|
value.reject { |flag, _| ["listChanged", "subscribe"].include?(flag.to_s) }
|
|
@@ -1224,7 +1300,7 @@ module MCP
|
|
|
1224
1300
|
apply_cache_metadata({ tools: page[:items], nextCursor: page[:next_cursor] }.compact)
|
|
1225
1301
|
end
|
|
1226
1302
|
|
|
1227
|
-
def call_tool(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
|
|
1303
|
+
def call_tool(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
|
|
1228
1304
|
tool_name = request[:name]
|
|
1229
1305
|
|
|
1230
1306
|
tool = tools[tool_name]
|
|
@@ -1259,12 +1335,13 @@ module MCP
|
|
|
1259
1335
|
response = call_tool_with_args(
|
|
1260
1336
|
tool,
|
|
1261
1337
|
arguments,
|
|
1262
|
-
server_context_with_meta(request),
|
|
1338
|
+
server_context_with_meta(request, auth_info: auth_info),
|
|
1263
1339
|
progress_token: progress_token,
|
|
1264
1340
|
session: session,
|
|
1265
1341
|
related_request_id: related_request_id,
|
|
1266
1342
|
cancellation: cancellation,
|
|
1267
1343
|
envelope: envelope,
|
|
1344
|
+
auth_info: auth_info,
|
|
1268
1345
|
retry_fields: mrtr_retry_fields(request),
|
|
1269
1346
|
)
|
|
1270
1347
|
# An SEP-2322 `input_required` result is not a tool result: output schema
|
|
@@ -1278,9 +1355,11 @@ module MCP
|
|
|
1278
1355
|
result,
|
|
1279
1356
|
content_provided: response.respond_to?(:content_provided?) && response.content_provided?,
|
|
1280
1357
|
)
|
|
1281
|
-
rescue RequestHandlerError, CancelledError
|
|
1358
|
+
rescue RequestHandlerError, CancelledError, OAuth::InsufficientScopeError
|
|
1282
1359
|
# CancelledError is intentionally not wrapped so `handle_request` can turn it into
|
|
1283
|
-
# `JsonRpcHandler::NO_RESPONSE` per the MCP cancellation spec
|
|
1360
|
+
# `JsonRpcHandler::NO_RESPONSE` per the MCP cancellation spec, and
|
|
1361
|
+
# `OAuth::InsufficientScopeError` so `require_scopes!` failures keep their
|
|
1362
|
+
# scope-naming message instead of the generic internal-error shape.
|
|
1284
1363
|
raise
|
|
1285
1364
|
rescue => e
|
|
1286
1365
|
# `e.message` is deliberately not included: it can carry internals (class, method
|
|
@@ -1300,7 +1379,7 @@ module MCP
|
|
|
1300
1379
|
apply_cache_metadata({ prompts: page[:items], nextCursor: page[:next_cursor] }.compact)
|
|
1301
1380
|
end
|
|
1302
1381
|
|
|
1303
|
-
def get_prompt(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
|
|
1382
|
+
def get_prompt(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
|
|
1304
1383
|
prompt_name = request[:name]
|
|
1305
1384
|
prompt = @prompts[prompt_name]
|
|
1306
1385
|
unless prompt
|
|
@@ -1328,6 +1407,7 @@ module MCP
|
|
|
1328
1407
|
related_request_id: related_request_id,
|
|
1329
1408
|
cancellation: cancellation,
|
|
1330
1409
|
envelope: envelope,
|
|
1410
|
+
auth_info: auth_info,
|
|
1331
1411
|
)
|
|
1332
1412
|
|
|
1333
1413
|
call_prompt_template_with_args(prompt, prompt_args, server_context)
|
|
@@ -1440,7 +1520,7 @@ module MCP
|
|
|
1440
1520
|
{ ttlMs: @ttl_ms || 0, cacheScope: @cache_scope || "private" }.merge(result)
|
|
1441
1521
|
end
|
|
1442
1522
|
|
|
1443
|
-
def complete(params, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
|
|
1523
|
+
def complete(params, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
|
|
1444
1524
|
validate_completion_params!(params)
|
|
1445
1525
|
|
|
1446
1526
|
result = dispatch_optional_context_handler(
|
|
@@ -1450,6 +1530,7 @@ module MCP
|
|
|
1450
1530
|
related_request_id: related_request_id,
|
|
1451
1531
|
cancellation: cancellation,
|
|
1452
1532
|
envelope: envelope,
|
|
1533
|
+
auth_info: auth_info,
|
|
1453
1534
|
)
|
|
1454
1535
|
|
|
1455
1536
|
normalize_completion_result(result)
|
|
@@ -1458,7 +1539,7 @@ module MCP
|
|
|
1458
1539
|
# Invokes `resources/read` via the registered handler. If the handler block opts in to `server_context:`,
|
|
1459
1540
|
# pass an `MCP::ServerContext` so the handler can observe cancellation via `server_context.cancelled?` or
|
|
1460
1541
|
# `server_context.raise_if_cancelled!`.
|
|
1461
|
-
def read_resource_contents(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil)
|
|
1542
|
+
def read_resource_contents(request, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
|
|
1462
1543
|
dispatch_optional_context_handler(
|
|
1463
1544
|
@handlers[Methods::RESOURCES_READ],
|
|
1464
1545
|
request,
|
|
@@ -1466,6 +1547,7 @@ module MCP
|
|
|
1466
1547
|
related_request_id: related_request_id,
|
|
1467
1548
|
cancellation: cancellation,
|
|
1468
1549
|
envelope: envelope,
|
|
1550
|
+
auth_info: auth_info,
|
|
1469
1551
|
)
|
|
1470
1552
|
end
|
|
1471
1553
|
|
|
@@ -1473,7 +1555,7 @@ module MCP
|
|
|
1473
1555
|
# `completion_handler`, `resources_subscribe_handler`, `resources_unsubscribe_handler`, or `define_custom_method`.
|
|
1474
1556
|
# Existing handlers that only accept `params` are called unchanged; handlers that declare a `server_context:`
|
|
1475
1557
|
# 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)
|
|
1558
|
+
def dispatch_optional_context_handler(handler, params, session: nil, related_request_id: nil, cancellation: nil, envelope: nil, auth_info: nil)
|
|
1477
1559
|
return handler.call(params) unless handler_declares_server_context?(handler)
|
|
1478
1560
|
|
|
1479
1561
|
server_context = build_server_context(
|
|
@@ -1482,6 +1564,7 @@ module MCP
|
|
|
1482
1564
|
related_request_id: related_request_id,
|
|
1483
1565
|
cancellation: cancellation,
|
|
1484
1566
|
envelope: envelope,
|
|
1567
|
+
auth_info: auth_info,
|
|
1485
1568
|
)
|
|
1486
1569
|
handler.call(params, server_context: server_context)
|
|
1487
1570
|
end
|
|
@@ -1506,13 +1589,13 @@ module MCP
|
|
|
1506
1589
|
|
|
1507
1590
|
# Builds an `MCP::ServerContext` used to give a handler access to session-scoped helpers
|
|
1508
1591
|
# (progress, cancellation, nested server-to-client requests).
|
|
1509
|
-
def build_server_context(request:, session:, related_request_id:, cancellation:, envelope: nil)
|
|
1592
|
+
def build_server_context(request:, session:, related_request_id:, cancellation:, envelope: nil, auth_info: nil)
|
|
1510
1593
|
meta_source = request.is_a?(Hash) ? request : {}
|
|
1511
1594
|
progress_token = meta_source.dig(:_meta, :progressToken)
|
|
1512
1595
|
progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
|
|
1513
1596
|
retry_fields = mrtr_retry_fields(meta_source)
|
|
1514
1597
|
ServerContext.new(
|
|
1515
|
-
server_context_with_meta(meta_source),
|
|
1598
|
+
server_context_with_meta(meta_source, auth_info: auth_info),
|
|
1516
1599
|
progress: progress,
|
|
1517
1600
|
notification_target: session,
|
|
1518
1601
|
related_request_id: related_request_id,
|
|
@@ -1520,6 +1603,7 @@ module MCP
|
|
|
1520
1603
|
envelope: envelope,
|
|
1521
1604
|
input_responses: retry_fields[:input_responses],
|
|
1522
1605
|
request_state: retry_fields[:request_state],
|
|
1606
|
+
auth_info: auth_info,
|
|
1523
1607
|
)
|
|
1524
1608
|
end
|
|
1525
1609
|
|
|
@@ -1579,7 +1663,7 @@ module MCP
|
|
|
1579
1663
|
end
|
|
1580
1664
|
end
|
|
1581
1665
|
|
|
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)
|
|
1666
|
+
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
1667
|
# Transports parse incoming JSON with `symbolize_names: true`, so `arguments` already arrives symbolized
|
|
1584
1668
|
# at every nesting level. This top-level transform only guards callers that hand in string-keyed top-level arguments;
|
|
1585
1669
|
# it does not recurse, and nested object keys remain symbols. Tools therefore receive symbol keys all the way down.
|
|
@@ -1597,6 +1681,7 @@ module MCP
|
|
|
1597
1681
|
envelope: envelope,
|
|
1598
1682
|
input_responses: retry_fields&.fetch(:input_responses, nil),
|
|
1599
1683
|
request_state: retry_fields&.fetch(:request_state, nil),
|
|
1684
|
+
auth_info: auth_info,
|
|
1600
1685
|
)
|
|
1601
1686
|
tool.call(**args, server_context: server_context)
|
|
1602
1687
|
else
|
|
@@ -1614,15 +1699,18 @@ module MCP
|
|
|
1614
1699
|
raw_result.is_a?(InputRequiredResult) ? raw_result : raw_result.to_h
|
|
1615
1700
|
end
|
|
1616
1701
|
|
|
1617
|
-
def server_context_with_meta(request)
|
|
1702
|
+
def server_context_with_meta(request, auth_info: nil)
|
|
1618
1703
|
meta = request[:_meta]
|
|
1619
|
-
if meta &&
|
|
1620
|
-
|
|
1621
|
-
|
|
1704
|
+
return server_context if meta.nil? && auth_info.nil?
|
|
1705
|
+
|
|
1706
|
+
if server_context.is_a?(Hash) || server_context.nil?
|
|
1707
|
+
context = (server_context || {}).dup
|
|
1708
|
+
context[:_meta] = meta if meta
|
|
1709
|
+
context[:auth_info] = auth_info if auth_info
|
|
1622
1710
|
context
|
|
1623
|
-
elsif meta && server_context.nil?
|
|
1624
|
-
{ _meta: meta }
|
|
1625
1711
|
else
|
|
1712
|
+
# A custom (non-Hash) user context is passed through untouched; handlers on that path read
|
|
1713
|
+
# the token via `server_context.auth_info` instead.
|
|
1626
1714
|
server_context
|
|
1627
1715
|
end
|
|
1628
1716
|
end
|
data/lib/mcp/server_context.rb
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
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
|
#
|
data/lib/mcp/server_session.rb
CHANGED
|
@@ -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
|
-
|
|
124
|
-
|
|
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.
|
|
@@ -265,14 +275,14 @@ module MCP
|
|
|
265
275
|
def notify_elicitation_complete(elicitation_id:)
|
|
266
276
|
send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id })
|
|
267
277
|
rescue => e
|
|
268
|
-
@server.
|
|
278
|
+
@server.configuration.exception_reporter.call(e, { notification: "elicitation_complete" })
|
|
269
279
|
end
|
|
270
280
|
|
|
271
281
|
# Sends a resource updated notification to this session only.
|
|
272
282
|
def notify_resources_updated(uri:)
|
|
273
283
|
send_to_transport(Methods::NOTIFICATIONS_RESOURCES_UPDATED, { "uri" => uri })
|
|
274
284
|
rescue => e
|
|
275
|
-
@server.
|
|
285
|
+
@server.configuration.exception_reporter.call(e, { notification: "resources_updated" })
|
|
276
286
|
end
|
|
277
287
|
|
|
278
288
|
# Sends a progress notification to this session only.
|
|
@@ -286,7 +296,7 @@ module MCP
|
|
|
286
296
|
|
|
287
297
|
send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params, related_request_id: related_request_id)
|
|
288
298
|
rescue => e
|
|
289
|
-
@server.
|
|
299
|
+
@server.configuration.exception_reporter.call(e, { notification: "progress" })
|
|
290
300
|
end
|
|
291
301
|
|
|
292
302
|
# Sends a log message notification to this session only.
|
|
@@ -310,7 +320,7 @@ module MCP
|
|
|
310
320
|
|
|
311
321
|
send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params, related_request_id: related_request_id)
|
|
312
322
|
rescue => e
|
|
313
|
-
@server.
|
|
323
|
+
@server.configuration.exception_reporter.call(e, { notification: "log_message" })
|
|
314
324
|
end
|
|
315
325
|
|
|
316
326
|
private
|
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
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.
|
|
4
|
+
version: 1.8.2
|
|
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.
|
|
122
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.8.2
|
|
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
|