mcp 0.14.0 → 0.16.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 +149 -1
- data/lib/json_rpc_handler.rb +6 -0
- data/lib/mcp/cancellation.rb +72 -0
- data/lib/mcp/cancelled_error.rb +13 -0
- data/lib/mcp/client/http.rb +99 -2
- data/lib/mcp/client/stdio.rb +100 -49
- data/lib/mcp/client.rb +41 -0
- data/lib/mcp/configuration.rb +22 -1
- data/lib/mcp/server/transports/stdio_transport.rb +7 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +63 -1
- data/lib/mcp/server.rb +160 -19
- data/lib/mcp/server_context.rb +12 -1
- data/lib/mcp/server_session.rb +105 -20
- data/lib/mcp/tool/schema.rb +22 -4
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- metadata +5 -4
- data/lib/mcp/transports/stdio.rb +0 -15
data/lib/mcp/server_session.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cancellation"
|
|
3
4
|
require_relative "methods"
|
|
4
5
|
|
|
5
6
|
module MCP
|
|
@@ -15,6 +16,48 @@ module MCP
|
|
|
15
16
|
@client = nil
|
|
16
17
|
@client_capabilities = nil
|
|
17
18
|
@logging_message_notification = nil
|
|
19
|
+
@in_flight = {}
|
|
20
|
+
@in_flight_mutex = Mutex.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Registers a `Cancellation` token for an in-flight request.
|
|
24
|
+
def register_in_flight(request_id)
|
|
25
|
+
return if request_id.nil?
|
|
26
|
+
|
|
27
|
+
cancellation = Cancellation.new(request_id: request_id)
|
|
28
|
+
@in_flight_mutex.synchronize { @in_flight[request_id] = cancellation }
|
|
29
|
+
cancellation
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unregister_in_flight(request_id)
|
|
33
|
+
return if request_id.nil?
|
|
34
|
+
|
|
35
|
+
@in_flight_mutex.synchronize { @in_flight.delete(request_id) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lookup_in_flight(request_id)
|
|
39
|
+
@in_flight_mutex.synchronize { @in_flight[request_id] }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Flips the `Cancellation` for a matching in-flight request received from the peer.
|
|
43
|
+
# Silently ignores unknown IDs per MCP spec (cancellation utilities, item 5).
|
|
44
|
+
def cancel_incoming(request_id:, reason: nil)
|
|
45
|
+
cancellation = lookup_in_flight(request_id)
|
|
46
|
+
cancellation&.cancel(reason: reason)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Sends `notifications/cancelled` to the peer for a previously-issued request.
|
|
50
|
+
# Also unblocks any transport-level `send_request` waiting on a response for `request_id`.
|
|
51
|
+
def cancel_request(request_id:, reason: nil)
|
|
52
|
+
params = { requestId: request_id }
|
|
53
|
+
params[:reason] = reason if reason
|
|
54
|
+
send_to_transport(Methods::NOTIFICATIONS_CANCELLED, params)
|
|
55
|
+
|
|
56
|
+
if @transport.respond_to?(:cancel_pending_request)
|
|
57
|
+
@transport.cancel_pending_request(request_id, reason: reason)
|
|
58
|
+
end
|
|
59
|
+
rescue => e
|
|
60
|
+
MCP.configuration.exception_reporter.call(e, { notification: "cancelled", request_id: request_id })
|
|
18
61
|
end
|
|
19
62
|
|
|
20
63
|
def handle(request)
|
|
@@ -78,6 +121,23 @@ module MCP
|
|
|
78
121
|
send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
|
|
79
122
|
end
|
|
80
123
|
|
|
124
|
+
# Sends `notifications/cancelled` to the peer for a nested server-to-client request
|
|
125
|
+
# that was started inside a now-cancelled parent request. `related_request_id`
|
|
126
|
+
# is the parent request id so the notification is routed to the same stream
|
|
127
|
+
# (e.g. the parent's POST response stream on `StreamableHTTPTransport`) rather than
|
|
128
|
+
# the GET SSE stream.
|
|
129
|
+
def send_peer_cancellation(nested_request_id:, related_request_id: nil, reason: nil)
|
|
130
|
+
params = { requestId: nested_request_id }
|
|
131
|
+
params[:reason] = reason if reason
|
|
132
|
+
send_to_transport(Methods::NOTIFICATIONS_CANCELLED, params, related_request_id: related_request_id)
|
|
133
|
+
|
|
134
|
+
if @transport.respond_to?(:cancel_pending_request)
|
|
135
|
+
@transport.cancel_pending_request(nested_request_id, reason: reason)
|
|
136
|
+
end
|
|
137
|
+
rescue => e
|
|
138
|
+
MCP.configuration.exception_reporter.call(e, { notification: "cancelled", request_id: nested_request_id })
|
|
139
|
+
end
|
|
140
|
+
|
|
81
141
|
# Sends an elicitation complete notification scoped to this session.
|
|
82
142
|
def notify_elicitation_complete(elicitation_id:)
|
|
83
143
|
send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id })
|
|
@@ -121,32 +181,57 @@ module MCP
|
|
|
121
181
|
|
|
122
182
|
private
|
|
123
183
|
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
# `@transport.send_notification(method, params, session_id: @session_id)` and
|
|
129
|
-
# add `**` to `Transport#send_notification` and `StdioTransport#send_notification`.
|
|
184
|
+
# Forwards `send_notification` to the transport with only the kwargs the transport's method signature
|
|
185
|
+
# actually accepts. Custom transports that implement the abstract `send_notification(method, params = nil)`
|
|
186
|
+
# contract continue to work unchanged; bundled transports that declare `session_id:` / `related_request_id:`
|
|
187
|
+
# receive the session-scoped routing information.
|
|
130
188
|
def send_to_transport(method, params, related_request_id: nil)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
kwargs = {
|
|
190
|
+
session_id: @session_id,
|
|
191
|
+
related_request_id: related_request_id,
|
|
192
|
+
}.compact
|
|
193
|
+
|
|
194
|
+
forward_to_transport(@transport.method(:send_notification), method, params, kwargs)
|
|
136
195
|
end
|
|
137
196
|
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
197
|
+
# Forwards `send_request` to the transport with only the kwargs the transport's method signature
|
|
198
|
+
# actually accepts. Custom transports that implement the abstract `send_request(method, params = nil)`
|
|
199
|
+
# contract continue to work; bundled transports that declare `session_id:` / `related_request_id:` /
|
|
200
|
+
# `parent_cancellation:` / `server_session:` receive the nested-cancellation plumbing.
|
|
201
|
+
# When `related_request_id` names an in-flight request, its `Cancellation` token is looked up
|
|
202
|
+
# so that cancelling the parent also cancels this nested server-to-client request.
|
|
144
203
|
def send_to_transport_request(method, params, related_request_id: nil)
|
|
145
|
-
|
|
146
|
-
|
|
204
|
+
parent_cancellation = related_request_id ? lookup_in_flight(related_request_id) : nil
|
|
205
|
+
|
|
206
|
+
kwargs = {
|
|
207
|
+
session_id: @session_id,
|
|
208
|
+
related_request_id: related_request_id,
|
|
209
|
+
parent_cancellation: parent_cancellation,
|
|
210
|
+
server_session: self,
|
|
211
|
+
}.compact
|
|
212
|
+
|
|
213
|
+
forward_to_transport(@transport.method(:send_request), method, params, kwargs)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Calls `transport_method(method, params, **supported)` where `supported` contains only the keys
|
|
217
|
+
# the transport's method signature accepts. This keeps bundled transports (which declare the new kwargs)
|
|
218
|
+
# working while preserving compatibility with custom transports that implement only the abstract
|
|
219
|
+
# `(method, params = nil)` contract.
|
|
220
|
+
def forward_to_transport(transport_method, method, params, kwargs)
|
|
221
|
+
parameters = transport_method.parameters
|
|
222
|
+
accepts_keyrest = parameters.any? { |type, _| type == :keyrest }
|
|
223
|
+
supported = if accepts_keyrest
|
|
224
|
+
kwargs
|
|
147
225
|
else
|
|
148
|
-
|
|
226
|
+
allowed = parameters.filter_map { |type, name| name if type == :key || type == :keyreq }
|
|
227
|
+
kwargs.slice(*allowed)
|
|
149
228
|
end
|
|
229
|
+
|
|
230
|
+
# Always splat `**supported` even when empty: on Ruby 2.7 the bare `transport_method.call(method, params)`
|
|
231
|
+
# form would let the trailing `params` Hash be auto-promoted to keyword arguments when the receiver
|
|
232
|
+
# accepts `**kwargs`, breaking handlers that rely on `params` arriving as a positional Hash.
|
|
233
|
+
# The explicit splat suppresses that conversion and is a no-op when `supported` is empty.
|
|
234
|
+
transport_method.call(method, params, **supported)
|
|
150
235
|
end
|
|
151
236
|
end
|
|
152
237
|
end
|
data/lib/mcp/tool/schema.rb
CHANGED
|
@@ -5,6 +5,12 @@ require "json-schema"
|
|
|
5
5
|
module MCP
|
|
6
6
|
class Tool
|
|
7
7
|
class Schema
|
|
8
|
+
# JSON Schema 2020-12 is the default dialect for MCP schema definitions
|
|
9
|
+
# per MCP 2025-11-25 (SEP-1613). Note: emission only — runtime validation
|
|
10
|
+
# is still performed against the JSON Schema draft-04 metaschema because
|
|
11
|
+
# the `json-schema` gem does not yet support 2020-12.
|
|
12
|
+
JSON_SCHEMA_2020_12_URI = "https://json-schema.org/draft/2020-12/schema"
|
|
13
|
+
|
|
8
14
|
attr_reader :schema
|
|
9
15
|
|
|
10
16
|
def initialize(schema = {})
|
|
@@ -18,17 +24,18 @@ module MCP
|
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def to_h
|
|
21
|
-
@schema
|
|
27
|
+
return @schema if @schema.key?(:"$schema")
|
|
28
|
+
|
|
29
|
+
{ "$schema": JSON_SCHEMA_2020_12_URI }.merge(@schema)
|
|
22
30
|
end
|
|
23
31
|
|
|
24
32
|
private
|
|
25
33
|
|
|
26
34
|
def fully_validate(data)
|
|
27
|
-
JSON::Validator.fully_validate(
|
|
35
|
+
JSON::Validator.fully_validate(schema_for_validation, data)
|
|
28
36
|
end
|
|
29
37
|
|
|
30
38
|
def validate_schema!
|
|
31
|
-
schema = to_h
|
|
32
39
|
gem_path = File.realpath(Gem.loaded_specs["json-schema"].full_gem_path)
|
|
33
40
|
schema_reader = JSON::Schema::Reader.new(
|
|
34
41
|
accept_uri: false,
|
|
@@ -38,11 +45,22 @@ module MCP
|
|
|
38
45
|
# Converts metaschema to a file URI for cross-platform compatibility
|
|
39
46
|
metaschema_uri = JSON::Util::URI.file_uri(metaschema_path.expand_path.cleanpath.to_s.tr("\\", "/"))
|
|
40
47
|
metaschema = metaschema_uri.to_s
|
|
41
|
-
errors = JSON::Validator.fully_validate(metaschema,
|
|
48
|
+
errors = JSON::Validator.fully_validate(metaschema, schema_for_validation, schema_reader: schema_reader)
|
|
42
49
|
if errors.any?
|
|
43
50
|
raise ArgumentError, "Invalid JSON Schema: #{errors.join(", ")}"
|
|
44
51
|
end
|
|
45
52
|
end
|
|
53
|
+
|
|
54
|
+
# The `json-schema` gem's draft-04 validator cannot resolve newer or unknown `$schema`
|
|
55
|
+
# dialect URIs. Strip the top-level `$schema` before validation so a dialect URI
|
|
56
|
+
# (whether SDK-injected by `to_h` or user-supplied) does not break the validator.
|
|
57
|
+
def schema_for_validation
|
|
58
|
+
return @schema unless @schema.key?(:"$schema")
|
|
59
|
+
|
|
60
|
+
copy = @schema.dup
|
|
61
|
+
copy.delete(:"$schema")
|
|
62
|
+
copy
|
|
63
|
+
end
|
|
46
64
|
end
|
|
47
65
|
end
|
|
48
66
|
end
|
data/lib/mcp/version.rb
CHANGED
data/lib/mcp.rb
CHANGED
|
@@ -8,6 +8,8 @@ require_relative "mcp/version"
|
|
|
8
8
|
|
|
9
9
|
module MCP
|
|
10
10
|
autoload :Annotations, "mcp/annotations"
|
|
11
|
+
autoload :Cancellation, "mcp/cancellation"
|
|
12
|
+
autoload :CancelledError, "mcp/cancelled_error"
|
|
11
13
|
autoload :Client, "mcp/client"
|
|
12
14
|
autoload :Content, "mcp/content"
|
|
13
15
|
autoload :Icon, "mcp/icon"
|
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: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Model Context Protocol
|
|
@@ -37,6 +37,8 @@ files:
|
|
|
37
37
|
- lib/json_rpc_handler.rb
|
|
38
38
|
- lib/mcp.rb
|
|
39
39
|
- lib/mcp/annotations.rb
|
|
40
|
+
- lib/mcp/cancellation.rb
|
|
41
|
+
- lib/mcp/cancelled_error.rb
|
|
40
42
|
- lib/mcp/client.rb
|
|
41
43
|
- lib/mcp/client/http.rb
|
|
42
44
|
- lib/mcp/client/paginated_result.rb
|
|
@@ -73,14 +75,13 @@ files:
|
|
|
73
75
|
- lib/mcp/tool/response.rb
|
|
74
76
|
- lib/mcp/tool/schema.rb
|
|
75
77
|
- lib/mcp/transport.rb
|
|
76
|
-
- lib/mcp/transports/stdio.rb
|
|
77
78
|
- lib/mcp/version.rb
|
|
78
79
|
homepage: https://ruby.sdk.modelcontextprotocol.io
|
|
79
80
|
licenses:
|
|
80
81
|
- Apache-2.0
|
|
81
82
|
metadata:
|
|
82
83
|
allowed_push_host: https://rubygems.org
|
|
83
|
-
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.
|
|
84
|
+
changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.16.0
|
|
84
85
|
homepage_uri: https://ruby.sdk.modelcontextprotocol.io
|
|
85
86
|
source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
|
|
86
87
|
bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues
|
|
@@ -99,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
99
100
|
- !ruby/object:Gem::Version
|
|
100
101
|
version: '0'
|
|
101
102
|
requirements: []
|
|
102
|
-
rubygems_version: 4.0.
|
|
103
|
+
rubygems_version: 4.0.10
|
|
103
104
|
specification_version: 4
|
|
104
105
|
summary: The official Ruby SDK for Model Context Protocol servers and clients
|
|
105
106
|
test_files: []
|
data/lib/mcp/transports/stdio.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../server/transports/stdio_transport"
|
|
4
|
-
|
|
5
|
-
warn <<~MESSAGE, uplevel: 3
|
|
6
|
-
Use `require "mcp/server/transports/stdio_transport"` instead of `require "mcp/transports/stdio"`.
|
|
7
|
-
Also use `MCP::Server::Transports::StdioTransport` instead of `MCP::Transports::StdioTransport`.
|
|
8
|
-
This API is deprecated and will be removed in a future release.
|
|
9
|
-
MESSAGE
|
|
10
|
-
|
|
11
|
-
module MCP
|
|
12
|
-
module Transports
|
|
13
|
-
StdioTransport = Server::Transports::StdioTransport
|
|
14
|
-
end
|
|
15
|
-
end
|