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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/json_rpc_handler.rb +37 -18
- 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 +23 -4
- 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 +517 -139
- data/lib/mcp/server.rb +92 -35
- data/lib/mcp/server_context.rb +26 -2
- data/lib/mcp/server_session.rb +14 -4
- 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,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
|
-
#
|
|
643
|
-
#
|
|
644
|
-
#
|
|
645
|
-
|
|
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:
|
|
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 &&
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
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.
|
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.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.
|
|
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
|