mcp 1.2.0 → 1.4.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.
- checksums.yaml +4 -4
- data/README.md +50 -2967
- data/lib/mcp/client/oauth/bounded_body.rb +67 -0
- data/lib/mcp/client/oauth/flow.rb +44 -10
- data/lib/mcp/client/oauth/id_jag_token_exchange.rb +12 -1
- data/lib/mcp/client/oauth.rb +1 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +137 -31
- data/lib/mcp/server.rb +83 -12
- data/lib/mcp/server_session.rb +27 -5
- data/lib/mcp/version.rb +1 -1
- metadata +3 -2
data/lib/mcp/server.rb
CHANGED
|
@@ -219,6 +219,7 @@ module MCP
|
|
|
219
219
|
@resources = resources
|
|
220
220
|
@resource_templates = resource_templates
|
|
221
221
|
@resource_index = index_resources_by_uri(resources)
|
|
222
|
+
@resources_list_handler = nil
|
|
222
223
|
@server_context = server_context
|
|
223
224
|
self.page_size = page_size
|
|
224
225
|
self.ttl_ms = ttl_ms
|
|
@@ -426,6 +427,22 @@ module MCP
|
|
|
426
427
|
@handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
|
|
427
428
|
end
|
|
428
429
|
|
|
430
|
+
# Sets a custom handler for `resources/list` requests, letting the visible list depend on request context such as
|
|
431
|
+
# the authenticated principal or granted scope. The block returns the resource collection to serve;
|
|
432
|
+
# the framework paginates it and stamps SEP-2549 cache hints exactly as it does for the constructor-provided resources,
|
|
433
|
+
# so the block returns only the array, not the paginated result.
|
|
434
|
+
# A block that declares a `server_context:` keyword receives an `MCP::ServerContext`. When no handler is set,
|
|
435
|
+
# the constructor-provided `resources` array is served unchanged.
|
|
436
|
+
#
|
|
437
|
+
# The block is invoked once per page, so it must return a stable ordering across the pages of one logical query;
|
|
438
|
+
# the cursor is a positional offset into the returned collection.
|
|
439
|
+
#
|
|
440
|
+
# @yield [params, server_context:] The request params, and an `MCP::ServerContext` when declared.
|
|
441
|
+
# @yieldreturn [Array<MCP::Resource>] The resources to paginate.
|
|
442
|
+
def resources_list_handler(&block)
|
|
443
|
+
@resources_list_handler = block
|
|
444
|
+
end
|
|
445
|
+
|
|
429
446
|
# Sets a custom handler for `resources/read` requests.
|
|
430
447
|
# The block receives the parsed request params and should return resource
|
|
431
448
|
# contents. The return value is set as the `contents` field of the response.
|
|
@@ -446,19 +463,25 @@ module MCP
|
|
|
446
463
|
end
|
|
447
464
|
|
|
448
465
|
# Sets a custom handler for `resources/subscribe` requests.
|
|
449
|
-
# The block receives the parsed request params. The
|
|
450
|
-
#
|
|
466
|
+
# The block receives the parsed request params. The response is an empty result, except that
|
|
467
|
+
# a `_meta` hash the block returns is passed through - the spec defines no other member for this result,
|
|
468
|
+
# so any other field the block returns is dropped. Nest a subscription identifier or other advisory data
|
|
469
|
+
# under `_meta`.
|
|
451
470
|
#
|
|
452
471
|
# @yield [params] The request params containing `:uri`.
|
|
472
|
+
# @yieldreturn [Hash, nil] Optionally `{ _meta: { ... } }`; any other shape yields an empty result.
|
|
453
473
|
def resources_subscribe_handler(&block)
|
|
454
474
|
@handlers[Methods::RESOURCES_SUBSCRIBE] = block
|
|
455
475
|
end
|
|
456
476
|
|
|
457
477
|
# Sets a custom handler for `resources/unsubscribe` requests.
|
|
458
|
-
# The block receives the parsed request params. The
|
|
459
|
-
#
|
|
478
|
+
# The block receives the parsed request params. The response is an empty result, except that
|
|
479
|
+
# a `_meta` hash the block returns is passed through - the spec defines no other member for this result,
|
|
480
|
+
# so any other field the block returns is dropped. Nest a subscription identifier or other advisory data
|
|
481
|
+
# under `_meta`.
|
|
460
482
|
#
|
|
461
483
|
# @yield [params] The request params containing `:uri`.
|
|
484
|
+
# @yieldreturn [Hash, nil] Optionally `{ _meta: { ... } }`; any other shape yields an empty result.
|
|
462
485
|
def resources_unsubscribe_handler(&block)
|
|
463
486
|
@handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
|
|
464
487
|
end
|
|
@@ -619,8 +642,24 @@ module MCP
|
|
|
619
642
|
|
|
620
643
|
# `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2),
|
|
621
644
|
# so do not track it in the in-flight registry.
|
|
622
|
-
cancellation =
|
|
623
|
-
|
|
645
|
+
cancellation = nil
|
|
646
|
+
if related_request_id && method != Methods::INITIALIZE && session
|
|
647
|
+
cancellation = session.register_in_flight(related_request_id)
|
|
648
|
+
|
|
649
|
+
# The spec puts the uniqueness obligation on the sender - "The request ID MUST NOT have been previously used by
|
|
650
|
+
# the requestor within the same session" - and says nothing about what a receiver does with a duplicate.
|
|
651
|
+
# Answering one is the only option that stays correct: the id routes request-scoped messages back to
|
|
652
|
+
# the request that caused them, and the transport's rule is that those messages "SHOULD relate to
|
|
653
|
+
# the originating client request", which a second live request under the same id makes impossible to honor
|
|
654
|
+
# for either of them. Refused the same way a duplicate `initialize` is, and for the same reason:
|
|
655
|
+
# so that a repeated id cannot silently displace state negotiated by the first one.
|
|
656
|
+
if cancellation.nil?
|
|
657
|
+
raise RequestHandlerError.new(
|
|
658
|
+
"Invalid Request: request id #{related_request_id.inspect} is already in flight",
|
|
659
|
+
request,
|
|
660
|
+
error_type: :invalid_request,
|
|
661
|
+
)
|
|
662
|
+
end
|
|
624
663
|
end
|
|
625
664
|
|
|
626
665
|
->(params) {
|
|
@@ -653,8 +692,9 @@ module MCP
|
|
|
653
692
|
contents.is_a?(InputRequiredResult) ? contents : build_read_resource_result(contents)
|
|
654
693
|
when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
|
|
655
694
|
validate_resource_subscription_params!(params)
|
|
656
|
-
dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
657
|
-
|
|
695
|
+
handler_result = dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
696
|
+
|
|
697
|
+
subscription_result(handler_result)
|
|
658
698
|
when Methods::TOOLS_CALL
|
|
659
699
|
call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation, envelope: envelope)
|
|
660
700
|
when Methods::PROMPTS_GET
|
|
@@ -727,7 +767,10 @@ module MCP
|
|
|
727
767
|
reported_exception = wrapped
|
|
728
768
|
raise wrapped
|
|
729
769
|
ensure
|
|
730
|
-
|
|
770
|
+
# `cancellation` is non-nil exactly when this request claimed the id above, so this also keeps `initialize`
|
|
771
|
+
# (which never registers) from evicting an in-flight registration under a reused id when the duplicate-`initialize`
|
|
772
|
+
# refusal raises out of the handler.
|
|
773
|
+
session&.unregister_in_flight(related_request_id, cancellation: cancellation) if related_request_id && cancellation
|
|
731
774
|
end
|
|
732
775
|
}
|
|
733
776
|
end
|
|
@@ -1080,6 +1123,18 @@ module MCP
|
|
|
1080
1123
|
end
|
|
1081
1124
|
end
|
|
1082
1125
|
|
|
1126
|
+
# The `resources/subscribe` and `resources/unsubscribe` result is an empty object except for the optional `_meta`
|
|
1127
|
+
# every result may carry: the TypeScript SDK validates it against `EmptyResultSchema.strict()`,
|
|
1128
|
+
# which rejects any other member, so only `_meta` is passed through from the handler. A handler that returns
|
|
1129
|
+
# anything else keeps the empty `{}` result it had before, so returning a subscription identifier or
|
|
1130
|
+
# other advisory data means nesting it under `_meta`.
|
|
1131
|
+
def subscription_result(handler_result)
|
|
1132
|
+
return {} unless handler_result.is_a?(Hash)
|
|
1133
|
+
|
|
1134
|
+
meta = handler_result[:_meta] || handler_result["_meta"]
|
|
1135
|
+
meta.is_a?(Hash) ? { _meta: meta } : {}
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1083
1138
|
def validate_initialize_params!(params)
|
|
1084
1139
|
unless params.is_a?(Hash)
|
|
1085
1140
|
raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)
|
|
@@ -1273,12 +1328,28 @@ module MCP
|
|
|
1273
1328
|
call_prompt_template_with_args(prompt, prompt_args, server_context)
|
|
1274
1329
|
end
|
|
1275
1330
|
|
|
1276
|
-
def list_resources(request)
|
|
1277
|
-
|
|
1331
|
+
def list_resources(request, server_context: nil)
|
|
1332
|
+
resources = if @resources_list_handler
|
|
1333
|
+
invoke_resources_list_handler(request, server_context)
|
|
1334
|
+
else
|
|
1335
|
+
@resources
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
page = paginate(resources, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
|
|
1278
1339
|
|
|
1279
1340
|
apply_cache_metadata({ resources: page[:items], nextCursor: page[:next_cursor] }.compact)
|
|
1280
1341
|
end
|
|
1281
1342
|
|
|
1343
|
+
# Calls the `resources_list_handler` block, forwarding `server_context:` only when the block opts in
|
|
1344
|
+
# by declaring the keyword (the same rule `dispatch_optional_context_handler` applies).
|
|
1345
|
+
def invoke_resources_list_handler(request, server_context)
|
|
1346
|
+
if handler_declares_server_context?(@resources_list_handler)
|
|
1347
|
+
@resources_list_handler.call(request, server_context: server_context)
|
|
1348
|
+
else
|
|
1349
|
+
@resources_list_handler.call(request)
|
|
1350
|
+
end
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1282
1353
|
# Default `resources/read` handler: routes to class-based resources and resource templates.
|
|
1283
1354
|
# Fully replaced when `resources_read_handler` is set. When no class-based resource or template is registered,
|
|
1284
1355
|
# unknown URIs keep the historical no-op `[]` response instead of raising.
|
|
@@ -1507,7 +1578,7 @@ module MCP
|
|
|
1507
1578
|
# Transports parse incoming JSON with `symbolize_names: true`, so `arguments` already arrives symbolized
|
|
1508
1579
|
# at every nesting level. This top-level transform only guards callers that hand in string-keyed top-level arguments;
|
|
1509
1580
|
# it does not recurse, and nested object keys remain symbols. Tools therefore receive symbol keys all the way down.
|
|
1510
|
-
# See docs/
|
|
1581
|
+
# See docs/server/tools.md ("Tool argument keys").
|
|
1511
1582
|
args = arguments&.transform_keys(&:to_sym) || {}
|
|
1512
1583
|
|
|
1513
1584
|
if accepts_server_context?(tool.method(:call))
|
data/lib/mcp/server_session.rb
CHANGED
|
@@ -58,19 +58,41 @@ module MCP
|
|
|
58
58
|
@era = era
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# Registers a `Cancellation` token for an in-flight request.
|
|
61
|
+
# Registers a `Cancellation` token for an in-flight request, or returns `nil` when `request_id` is already in flight.
|
|
62
|
+
# The request id is the only key that routes request-scoped notifications, server-to-client requests,
|
|
63
|
+
# and `notifications/cancelled` back to the request that caused them, so a second live request under the same id
|
|
64
|
+
# has no destination of its own. Rather than let the newcomer displace the registration, report the collision
|
|
65
|
+
# and leave the first request intact; the caller turns that into an Invalid Request.
|
|
62
66
|
def register_in_flight(request_id)
|
|
63
67
|
return if request_id.nil?
|
|
64
68
|
|
|
65
69
|
cancellation = Cancellation.new(request_id: request_id)
|
|
66
|
-
@in_flight_mutex.synchronize
|
|
67
|
-
|
|
70
|
+
registered = @in_flight_mutex.synchronize do
|
|
71
|
+
next false if @in_flight.key?(request_id)
|
|
72
|
+
|
|
73
|
+
@in_flight[request_id] = cancellation
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
registered ? cancellation : nil
|
|
68
78
|
end
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
# Removes an in-flight registration. Passing the `Cancellation` that `register_in_flight` returned removes
|
|
81
|
+
# the entry only while it is still that one, so a request can never evict a registration it does not own.
|
|
82
|
+
def unregister_in_flight(request_id, cancellation: nil)
|
|
71
83
|
return if request_id.nil?
|
|
72
84
|
|
|
73
|
-
@in_flight_mutex.synchronize
|
|
85
|
+
@in_flight_mutex.synchronize do
|
|
86
|
+
next if cancellation && !@in_flight[request_id].equal?(cancellation)
|
|
87
|
+
|
|
88
|
+
@in_flight.delete(request_id)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Whether `request_id` is currently in flight, so a transport can refuse a colliding request
|
|
93
|
+
# before registering any state of its own for it.
|
|
94
|
+
def in_flight?(request_id)
|
|
95
|
+
!lookup_in_flight(request_id).nil?
|
|
74
96
|
end
|
|
75
97
|
|
|
76
98
|
def lookup_in_flight(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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Model Context Protocol
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- lib/mcp/client/mcp_param_headers.rb
|
|
47
47
|
- lib/mcp/client/modern_envelope.rb
|
|
48
48
|
- lib/mcp/client/oauth.rb
|
|
49
|
+
- lib/mcp/client/oauth/bounded_body.rb
|
|
49
50
|
- lib/mcp/client/oauth/client_credentials_provider.rb
|
|
50
51
|
- lib/mcp/client/oauth/cross_app_access_provider.rb
|
|
51
52
|
- lib/mcp/client/oauth/discovery.rb
|
|
@@ -106,7 +107,7 @@ licenses:
|
|
|
106
107
|
- Apache-2.0
|
|
107
108
|
metadata:
|
|
108
109
|
allowed_push_host: https://rubygems.org
|
|
109
|
-
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.
|
|
110
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.4.0
|
|
110
111
|
homepage_uri: https://ruby.sdk.modelcontextprotocol.io
|
|
111
112
|
source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
112
113
|
bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
|