mcp 1.8.1 → 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/lib/json_rpc_handler.rb +32 -2
- data/lib/mcp/client/stdio.rb +12 -2
- data/lib/mcp/server/transports/streamable_http_transport.rb +29 -10
- data/lib/mcp/server.rb +32 -1
- data/lib/mcp/server_session.rb +4 -4
- data/lib/mcp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e99b2f4d4d5727123d1ae167586134b1f06498f975ed2b16cb2f7e1c413d1271
|
|
4
|
+
data.tar.gz: 9ea2939b0f5fd94cad620376561def6cc0db832f0f62f990cdf66799c91c3bb6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acc1d2aa4fa8c76b2028cf2cc74b806b08ffc3e42a1e41d48326c53145fc6d3c95e6e724db9960e808b5c2f80330d4973caec143d97bf644f0f800348782476c
|
|
7
|
+
data.tar.gz: ddf8af09d31cb72577ceb3b4f2ef7fb90f0e813790966d532cc82690d9e9f56847cf8f391c36e860c6f51747dd795394f943941d6ecb44045e9ba424819b5854
|
data/lib/json_rpc_handler.rb
CHANGED
|
@@ -53,7 +53,9 @@ module JsonRpcHandler
|
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
# `on_generate_error`, when given, is called with the `JSON::GeneratorError` of a response that cannot be generated,
|
|
57
|
+
# before that response is replaced with Internal error.
|
|
58
|
+
def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, on_generate_error: nil, &method_finder)
|
|
57
59
|
begin
|
|
58
60
|
begin
|
|
59
61
|
request = JSON.parse(request_json, symbolize_names: true)
|
|
@@ -77,7 +79,35 @@ module JsonRpcHandler
|
|
|
77
79
|
})
|
|
78
80
|
end
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
begin
|
|
83
|
+
response&.to_json
|
|
84
|
+
rescue JSON::GeneratorError => e
|
|
85
|
+
on_generate_error&.call(e)
|
|
86
|
+
encodable_response(response, id_validation_pattern: id_validation_pattern).to_json
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `response` with each part JSON cannot generate replaced with Internal error. A batch is replaced item by item,
|
|
91
|
+
# so the other items keep their results.
|
|
92
|
+
def encodable_response(response, id_validation_pattern: nil)
|
|
93
|
+
return response.map { |item| encodable_response(item, id_validation_pattern: id_validation_pattern) } if response.is_a?(Array)
|
|
94
|
+
|
|
95
|
+
response.to_json
|
|
96
|
+
response
|
|
97
|
+
rescue JSON::GeneratorError
|
|
98
|
+
internal_error_for(response, id_validation_pattern: id_validation_pattern)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The error answering a request whose response cannot be generated: a result or an error holding a string that
|
|
102
|
+
# is not valid UTF-8, typically one the request carried and the SDK or a handler repeated, such as a tool name
|
|
103
|
+
# in its Tool not found error. It carries the request id when the response has one, which was validated before
|
|
104
|
+
# the method ran.
|
|
105
|
+
def internal_error_for(response, id_validation_pattern: nil)
|
|
106
|
+
id = response[:id] if response.is_a?(Hash)
|
|
107
|
+
error_response(id: id.nil? ? :unknown_id : id, id_validation_pattern: id_validation_pattern, error: {
|
|
108
|
+
code: ErrorCode::INTERNAL_ERROR,
|
|
109
|
+
message: "Internal error",
|
|
110
|
+
})
|
|
81
111
|
end
|
|
82
112
|
|
|
83
113
|
def process_request(request, id_validation_pattern:, &method_finder)
|
data/lib/mcp/client/stdio.rb
CHANGED
|
@@ -434,7 +434,7 @@ module MCP
|
|
|
434
434
|
line = read_line(method, params)
|
|
435
435
|
raise_connection_error!(method, params) if line.nil?
|
|
436
436
|
|
|
437
|
-
parsed = JSON.parse(line
|
|
437
|
+
parsed = JSON.parse(strip_frame(line))
|
|
438
438
|
|
|
439
439
|
# A frame carrying `method` is a request or notification, never the awaited response,
|
|
440
440
|
# whatever its id says: JSON-RPC id spaces are per sender, so a server-chosen ping id
|
|
@@ -484,8 +484,10 @@ module MCP
|
|
|
484
484
|
parsed["method"] == MCP::Methods::PING && json_rpc_id?(parsed["id"]) && JsonRpcHandler.valid_version?(parsed["jsonrpc"])
|
|
485
485
|
end
|
|
486
486
|
|
|
487
|
+
# An id the pong could not write back is skipped too: a String that is not valid UTF-8, or a number
|
|
488
|
+
# such as `1e400`, which the parser reads as an infinite Float that JSON cannot represent.
|
|
487
489
|
def json_rpc_id?(id)
|
|
488
|
-
id.is_a?(String) || id.is_a?(
|
|
490
|
+
(id.is_a?(String) && id.valid_encoding?) || id.is_a?(Integer) || (id.is_a?(Float) && id.finite?)
|
|
489
491
|
end
|
|
490
492
|
|
|
491
493
|
def ensure_running!
|
|
@@ -534,6 +536,14 @@ module MCP
|
|
|
534
536
|
)
|
|
535
537
|
end
|
|
536
538
|
|
|
539
|
+
# Strips a frame as bytes. The server's stdout is read as UTF-8, and `String#strip` raises for a UTF-8 string
|
|
540
|
+
# holding an invalid byte at either end on every supported Ruby, and anywhere in it from Ruby 3.2 on, which would
|
|
541
|
+
# surface as that exception instead of the client's own error. As bytes, the frame reaches the parser, which
|
|
542
|
+
# refuses what is not JSON with the client's parse failure.
|
|
543
|
+
def strip_frame(line)
|
|
544
|
+
line.b.strip.force_encoding(Encoding::UTF_8)
|
|
545
|
+
end
|
|
546
|
+
|
|
537
547
|
def raise_connection_error!(method, params)
|
|
538
548
|
raise RequestHandlerError.new(
|
|
539
549
|
"Server process closed stdout unexpectedly",
|
|
@@ -521,12 +521,19 @@ module MCP
|
|
|
521
521
|
# a per-request sink for its ephemeral session and flushes it as SSE frames ahead of the final response.
|
|
522
522
|
# Checked before the stateless guard, since modern requests are served in stateless deployments too.
|
|
523
523
|
# The sink is bounded; past `MAX_MODERN_REQUEST_NOTIFICATIONS` the notification is dropped as
|
|
524
|
-
# non-delivery (`false`), never handed to the legacy paths below.
|
|
524
|
+
# non-delivery (`false`), never handed to the legacy paths below. It holds each notification as JSON,
|
|
525
|
+
# generated here so that one that cannot be generated is dropped on its own instead of failing
|
|
526
|
+
# the whole response when the sink is flushed.
|
|
525
527
|
sink = @mutex.synchronize { session_id && @modern_request_sinks[session_id] }
|
|
526
528
|
if sink
|
|
527
529
|
return false if sink.size >= MAX_MODERN_REQUEST_NOTIFICATIONS
|
|
528
530
|
|
|
529
|
-
|
|
531
|
+
begin
|
|
532
|
+
sink << notification.to_json
|
|
533
|
+
rescue JSON::GeneratorError => e
|
|
534
|
+
MCP.configuration.exception_reporter.call(e, { session_id: session_id, error: "Failed to generate notification" })
|
|
535
|
+
return false
|
|
536
|
+
end
|
|
530
537
|
return true
|
|
531
538
|
end
|
|
532
539
|
|
|
@@ -931,16 +938,26 @@ module MCP
|
|
|
931
938
|
# `nil` covers notifications and cancellation-suppressed responses; ack with 202 like the legacy notification path.
|
|
932
939
|
return handle_accepted if response.nil?
|
|
933
940
|
|
|
941
|
+
# A response that cannot be generated answers its request with Internal error, as `JsonRpcHandler.handle_json`
|
|
942
|
+
# does on the legacy paths, instead of failing the whole exchange with a 500.
|
|
943
|
+
response_json = begin
|
|
944
|
+
response.to_json
|
|
945
|
+
rescue JSON::GeneratorError => e
|
|
946
|
+
MCP.configuration.exception_reporter.call(e, { error: "Failed to generate response" })
|
|
947
|
+
response = JsonRpcHandler.encodable_response(response)
|
|
948
|
+
response.to_json
|
|
949
|
+
end
|
|
950
|
+
|
|
934
951
|
# Notifications a handler emitted during the request ride the request's own response stream as SSE frames ahead of
|
|
935
952
|
# the final response (SEP-2575); a request that emitted none keeps the single JSON exchange and its HTTP status ladder.
|
|
936
953
|
# Delivery is buffered: frames flush after the handler returns, preserving order but not real-time interleaving
|
|
937
954
|
# (the TypeScript and Python SDKs stream live), and the sink grows with the handler's notification count.
|
|
938
955
|
# Live streaming can follow without changing the wire shape.
|
|
939
956
|
if notifications.empty?
|
|
940
|
-
[modern_http_status(response), { "content-type" => "application/json" }, [
|
|
957
|
+
[modern_http_status(response), { "content-type" => "application/json" }, [response_json]]
|
|
941
958
|
else
|
|
942
|
-
frames = notifications + [
|
|
943
|
-
sse_body = frames.map { |frame| "event: message\ndata: #{frame
|
|
959
|
+
frames = notifications + [response_json]
|
|
960
|
+
sse_body = frames.map { |frame| "event: message\ndata: #{frame}\n\n" }.join
|
|
944
961
|
[200, SSE_HEADERS.dup, [sse_body]]
|
|
945
962
|
end
|
|
946
963
|
rescue StandardError => e
|
|
@@ -1034,7 +1051,7 @@ module MCP
|
|
|
1034
1051
|
|
|
1035
1052
|
begin
|
|
1036
1053
|
if RequestEnvelope.modern?(params)
|
|
1037
|
-
RequestEnvelope.parse!(params, request: params)
|
|
1054
|
+
envelope = RequestEnvelope.parse!(params, request: params)
|
|
1038
1055
|
else
|
|
1039
1056
|
return invalid_request_response("Invalid Request: modern requests require the SEP-2575 `_meta` envelope")
|
|
1040
1057
|
end
|
|
@@ -1074,7 +1091,7 @@ module MCP
|
|
|
1074
1091
|
return too_many_listen_subscriptions_response(request_id)
|
|
1075
1092
|
end
|
|
1076
1093
|
|
|
1077
|
-
honored = honored_filter(filter)
|
|
1094
|
+
honored = honored_filter(filter, meta: params[:_meta], envelope: envelope, auth_info: auth_info)
|
|
1078
1095
|
|
|
1079
1096
|
# The acknowledgement echoes the honored filter, whose resource URIs are client-supplied strings,
|
|
1080
1097
|
# so it is encoded before the stream is committed to: a URI that cannot be written as JSON is
|
|
@@ -1321,8 +1338,9 @@ module MCP
|
|
|
1321
1338
|
|
|
1322
1339
|
# Per SEP-2575, the server MUST NOT send notification types the client has not requested,
|
|
1323
1340
|
# and the acknowledgement only includes types the server actually supports
|
|
1324
|
-
# (derived from its declared capabilities).
|
|
1325
|
-
|
|
1341
|
+
# (derived from its declared capabilities). Resource URIs are further narrowed to those
|
|
1342
|
+
# the server's `resources_subscribe_handler` accepts; the field is omitted when none remain.
|
|
1343
|
+
def honored_filter(filter, meta: nil, envelope: nil, auth_info: nil)
|
|
1326
1344
|
capabilities = @server.capabilities
|
|
1327
1345
|
honored = {}
|
|
1328
1346
|
honored[:toolsListChanged] = true if filter[:toolsListChanged] && capability_flag?(capabilities, :tools, :listChanged)
|
|
@@ -1331,7 +1349,8 @@ module MCP
|
|
|
1331
1349
|
|
|
1332
1350
|
subscriptions = filter[:resourceSubscriptions]
|
|
1333
1351
|
if capability_flag?(capabilities, :resources, :subscribe) && subscriptions.is_a?(Array) && !subscriptions.empty?
|
|
1334
|
-
|
|
1352
|
+
uris = @server.subscribable_resource_uris(subscriptions, meta: meta, envelope: envelope, auth_info: auth_info)
|
|
1353
|
+
honored[:resourceSubscriptions] = uris unless uris.empty?
|
|
1335
1354
|
end
|
|
1336
1355
|
|
|
1337
1356
|
honored
|
data/lib/mcp/server.rb
CHANGED
|
@@ -303,7 +303,8 @@ module MCP
|
|
|
303
303
|
# for the current request, as in `handle`.
|
|
304
304
|
# @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
|
|
305
305
|
def handle_json(request, session: nil, auth_info: nil)
|
|
306
|
-
|
|
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|
|
|
307
308
|
handle_request(request, method, session: session, related_request_id: request_id, auth_info: auth_info)
|
|
308
309
|
end
|
|
309
310
|
end
|
|
@@ -500,6 +501,36 @@ module MCP
|
|
|
500
501
|
@handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
|
|
501
502
|
end
|
|
502
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
|
+
|
|
503
534
|
def build_sampling_params(
|
|
504
535
|
capabilities,
|
|
505
536
|
messages:,
|
data/lib/mcp/server_session.rb
CHANGED
|
@@ -275,14 +275,14 @@ module MCP
|
|
|
275
275
|
def notify_elicitation_complete(elicitation_id:)
|
|
276
276
|
send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id })
|
|
277
277
|
rescue => e
|
|
278
|
-
@server.
|
|
278
|
+
@server.configuration.exception_reporter.call(e, { notification: "elicitation_complete" })
|
|
279
279
|
end
|
|
280
280
|
|
|
281
281
|
# Sends a resource updated notification to this session only.
|
|
282
282
|
def notify_resources_updated(uri:)
|
|
283
283
|
send_to_transport(Methods::NOTIFICATIONS_RESOURCES_UPDATED, { "uri" => uri })
|
|
284
284
|
rescue => e
|
|
285
|
-
@server.
|
|
285
|
+
@server.configuration.exception_reporter.call(e, { notification: "resources_updated" })
|
|
286
286
|
end
|
|
287
287
|
|
|
288
288
|
# Sends a progress notification to this session only.
|
|
@@ -296,7 +296,7 @@ module MCP
|
|
|
296
296
|
|
|
297
297
|
send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params, related_request_id: related_request_id)
|
|
298
298
|
rescue => e
|
|
299
|
-
@server.
|
|
299
|
+
@server.configuration.exception_reporter.call(e, { notification: "progress" })
|
|
300
300
|
end
|
|
301
301
|
|
|
302
302
|
# Sends a log message notification to this session only.
|
|
@@ -320,7 +320,7 @@ module MCP
|
|
|
320
320
|
|
|
321
321
|
send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params, related_request_id: related_request_id)
|
|
322
322
|
rescue => e
|
|
323
|
-
@server.
|
|
323
|
+
@server.configuration.exception_reporter.call(e, { notification: "log_message" })
|
|
324
324
|
end
|
|
325
325
|
|
|
326
326
|
private
|
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.8.
|
|
4
|
+
version: 1.8.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Model Context Protocol
|
|
@@ -119,7 +119,7 @@ licenses:
|
|
|
119
119
|
- Apache-2.0
|
|
120
120
|
metadata:
|
|
121
121
|
allowed_push_host: https://rubygems.org
|
|
122
|
-
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.8.
|
|
122
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v1.8.2
|
|
123
123
|
homepage_uri: https://ruby.sdk.modelcontextprotocol.io
|
|
124
124
|
source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
125
125
|
bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
|