mcp 0.14.0 → 0.15.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 +136 -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 +40 -0
- 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 +148 -17
- data/lib/mcp/server_context.rb +12 -1
- data/lib/mcp/server_session.rb +105 -20
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- metadata +4 -3
- data/lib/mcp/transports/stdio.rb +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d7b34843d1b915df5d3df424b025c93bba1d50a5e19486f2dcd4a646227a749
|
|
4
|
+
data.tar.gz: 4327576e74518a21d1dbe57f7f22a6f17c8e486b7a9fb9c281c11d206dcd007e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afe69100125bcc47dbfca963983eca401f5bf29b31c01748dd1a5e99537542a4cb2b021573d3635c94bb410cdd430c0e3ff023796d91299da366cc32c7999fba
|
|
7
|
+
data.tar.gz: 815153266ba50a7e3045d7058af383d2ba852e1eefbaa62bc78299da89621bb1c0e22c5d2738f56470f50b4e8bf85f0d83135b1789f2faa9c841cff5e29fa766
|
data/README.md
CHANGED
|
@@ -41,6 +41,7 @@ It implements the Model Context Protocol specification, handling model context r
|
|
|
41
41
|
- Supports roots (server-to-client filesystem boundary queries)
|
|
42
42
|
- Supports sampling (server-to-client LLM completion requests)
|
|
43
43
|
- Supports cursor-based pagination for list operations
|
|
44
|
+
- Supports server-side cancellation of in-flight requests (notifications/cancelled)
|
|
44
45
|
|
|
45
46
|
### Supported Methods
|
|
46
47
|
|
|
@@ -1096,9 +1097,137 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
|
|
|
1096
1097
|
- `notifications/tools/list_changed`
|
|
1097
1098
|
- `notifications/prompts/list_changed`
|
|
1098
1099
|
- `notifications/resources/list_changed`
|
|
1100
|
+
- `notifications/cancelled`
|
|
1099
1101
|
- `notifications/progress`
|
|
1100
1102
|
- `notifications/message`
|
|
1101
1103
|
|
|
1104
|
+
### Cancellation
|
|
1105
|
+
|
|
1106
|
+
The MCP Ruby SDK supports server-side handling of the
|
|
1107
|
+
[MCP `notifications/cancelled` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation).
|
|
1108
|
+
When a client sends `notifications/cancelled` for an in-flight request, the server stops
|
|
1109
|
+
processing cooperatively and suppresses the JSON-RPC response for that request.
|
|
1110
|
+
|
|
1111
|
+
Cancellation is cooperative: the SDK does not forcibly terminate tool code. Instead,
|
|
1112
|
+
a `MCP::Cancellation` token is threaded through `server_context`, and long-running tools
|
|
1113
|
+
poll it to exit early. When a tool returns after cancellation has been observed,
|
|
1114
|
+
the server suppresses the JSON-RPC response, matching the spec. The `initialize` request
|
|
1115
|
+
is never cancellable per the spec.
|
|
1116
|
+
|
|
1117
|
+
> [!NOTE]
|
|
1118
|
+
> Client-initiated cancellation (`Client#cancel` equivalent that would also abort
|
|
1119
|
+
> the calling thread's wait) is not yet implemented. Sending `notifications/cancelled`
|
|
1120
|
+
> from the client side can be done by constructing the notification payload and writing it
|
|
1121
|
+
> directly through the transport, but the calling thread does not yet unwind automatically.
|
|
1122
|
+
> This is tracked as a follow-up.
|
|
1123
|
+
|
|
1124
|
+
#### Server-Side: Handlers that Check for Cancellation
|
|
1125
|
+
|
|
1126
|
+
Any handler that opts in to `server_context:` - tools (`Tool.call`), prompt templates,
|
|
1127
|
+
`resources_read_handler`, `completion_handler`, `resources_subscribe_handler`,
|
|
1128
|
+
`resources_unsubscribe_handler`, and `define_custom_method` blocks - receives
|
|
1129
|
+
an `MCP::ServerContext` wired to the in-flight request's cancellation token.
|
|
1130
|
+
Handlers check `cancelled?` in their work loop, or call `raise_if_cancelled!` to raise
|
|
1131
|
+
`MCP::CancelledError` at a safe point:
|
|
1132
|
+
|
|
1133
|
+
```ruby
|
|
1134
|
+
class LongRunningTool < MCP::Tool
|
|
1135
|
+
description "A tool that supports cancellation"
|
|
1136
|
+
input_schema(properties: { count: { type: "integer" } }, required: ["count"])
|
|
1137
|
+
|
|
1138
|
+
def self.call(count:, server_context:)
|
|
1139
|
+
count.times do |i|
|
|
1140
|
+
# Exit early if the client has sent `notifications/cancelled`.
|
|
1141
|
+
break if server_context.cancelled?
|
|
1142
|
+
|
|
1143
|
+
do_work(i)
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
MCP::Tool::Response.new([{ type: "text", text: "Done" }])
|
|
1147
|
+
end
|
|
1148
|
+
end
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
Alternatively, raise at the next safe point with `raise_if_cancelled!`:
|
|
1152
|
+
|
|
1153
|
+
```ruby
|
|
1154
|
+
def self.call(count:, server_context:)
|
|
1155
|
+
count.times do |i|
|
|
1156
|
+
server_context.raise_if_cancelled!
|
|
1157
|
+
|
|
1158
|
+
do_work(i)
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
MCP::Tool::Response.new([{ type: "text", text: "Done" }])
|
|
1162
|
+
end
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
When a handler observes cancellation (either by returning early with `cancelled?` or
|
|
1166
|
+
by raising `MCP::CancelledError` via `raise_if_cancelled!`), the server drops the response and
|
|
1167
|
+
no JSON-RPC result is sent to the client.
|
|
1168
|
+
|
|
1169
|
+
The same pattern works for other handler types:
|
|
1170
|
+
|
|
1171
|
+
```ruby
|
|
1172
|
+
# resources/read
|
|
1173
|
+
server.resources_read_handler do |params, server_context:|
|
|
1174
|
+
server_context.raise_if_cancelled!
|
|
1175
|
+
# read the resource
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
# completion/complete
|
|
1179
|
+
server.completion_handler do |params, server_context:|
|
|
1180
|
+
server_context.raise_if_cancelled!
|
|
1181
|
+
# compute completions
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
# custom method
|
|
1185
|
+
server.define_custom_method(method_name: "custom/slow") do |params, server_context:|
|
|
1186
|
+
server_context.raise_if_cancelled!
|
|
1187
|
+
# do work
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
# prompts (via Prompt subclass)
|
|
1191
|
+
class SlowPrompt < MCP::Prompt
|
|
1192
|
+
prompt_name "slow_prompt"
|
|
1193
|
+
|
|
1194
|
+
def self.template(args, server_context:)
|
|
1195
|
+
server_context.raise_if_cancelled!
|
|
1196
|
+
MCP::Prompt::Result.new(messages: [])
|
|
1197
|
+
end
|
|
1198
|
+
end
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
Handlers that do not declare a `server_context:` keyword continue to work unchanged -
|
|
1202
|
+
the opt-in detection only wraps the context when the block signature asks for it.
|
|
1203
|
+
|
|
1204
|
+
#### Nested Server-to-Client Requests Are Cancelled Automatically
|
|
1205
|
+
|
|
1206
|
+
When a tool handler is waiting on a nested server-to-client request
|
|
1207
|
+
(`server_context.create_sampling_message`, `create_form_elicitation`, or
|
|
1208
|
+
`create_url_elicitation`), cancelling the parent tool call automatically raises
|
|
1209
|
+
`MCP::CancelledError` from the nested call, so the tool does not need to wrap it
|
|
1210
|
+
in its own `cancelled?` checks:
|
|
1211
|
+
|
|
1212
|
+
```ruby
|
|
1213
|
+
def self.call(server_context:)
|
|
1214
|
+
result = server_context.create_sampling_message(messages: messages, max_tokens: 100)
|
|
1215
|
+
# If the parent tools/call is cancelled while waiting above, MCP::CancelledError
|
|
1216
|
+
# is raised here and the tool can let it propagate or clean up as needed.
|
|
1217
|
+
MCP::Tool::Response.new([{ type: "text", text: result[:content][:text] }])
|
|
1218
|
+
rescue MCP::CancelledError
|
|
1219
|
+
# Optional: run cleanup. Re-raising (or letting it propagate) is fine; the server
|
|
1220
|
+
# will still suppress the JSON-RPC response per the MCP spec.
|
|
1221
|
+
raise
|
|
1222
|
+
end
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
Nested cancellation propagation is supported on `StreamableHTTPTransport` only.
|
|
1226
|
+
`StdioTransport` is single-threaded and blocks on `$stdin.gets`, so a nested
|
|
1227
|
+
`server_context.create_sampling_message` inside a tool runs to completion even if
|
|
1228
|
+
the parent `tools/call` is cancelled. The parent tool itself still observes cancellation
|
|
1229
|
+
via `server_context.cancelled?` between nested calls.
|
|
1230
|
+
|
|
1102
1231
|
### Ping
|
|
1103
1232
|
|
|
1104
1233
|
The MCP Ruby SDK supports the
|
|
@@ -1597,7 +1726,7 @@ This class supports:
|
|
|
1597
1726
|
- Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
|
|
1598
1727
|
- Resource listing via the `resources/list` method (`MCP::Client#resources`)
|
|
1599
1728
|
- Resource template listing via the `resources/templates/list` method (`MCP::Client#resource_templates`)
|
|
1600
|
-
- Resource reading via the `resources/read` method (`MCP::Client#
|
|
1729
|
+
- Resource reading via the `resources/read` method (`MCP::Client#read_resource`)
|
|
1601
1730
|
- Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
|
|
1602
1731
|
- Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
|
|
1603
1732
|
- Completion requests via the `completion/complete` method (`MCP::Client#complete`)
|
|
@@ -1653,6 +1782,9 @@ stdio_transport = MCP::Client::Stdio.new(
|
|
|
1653
1782
|
)
|
|
1654
1783
|
client = MCP::Client.new(transport: stdio_transport)
|
|
1655
1784
|
|
|
1785
|
+
# Perform the MCP initialization handshake before sending any requests.
|
|
1786
|
+
client.connect
|
|
1787
|
+
|
|
1656
1788
|
# List available tools.
|
|
1657
1789
|
tools = client.tools
|
|
1658
1790
|
tools.each do |tool|
|
|
@@ -1693,6 +1825,9 @@ Example usage:
|
|
|
1693
1825
|
http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
|
|
1694
1826
|
client = MCP::Client.new(transport: http_transport)
|
|
1695
1827
|
|
|
1828
|
+
# Perform the MCP initialization handshake before sending any requests.
|
|
1829
|
+
client.connect
|
|
1830
|
+
|
|
1696
1831
|
# List available tools
|
|
1697
1832
|
tools = client.tools
|
|
1698
1833
|
tools.each do |tool|
|
data/lib/json_rpc_handler.rb
CHANGED
|
@@ -18,6 +18,11 @@ module JsonRpcHandler
|
|
|
18
18
|
|
|
19
19
|
DEFAULT_ALLOWED_ID_CHARACTERS = /\A[a-zA-Z0-9_-]+\z/
|
|
20
20
|
|
|
21
|
+
# Sentinel return value from a handler. When a handler returns this,
|
|
22
|
+
# `process_request` emits no JSON-RPC response for the request,
|
|
23
|
+
# matching the notification-style semantics (id is ignored).
|
|
24
|
+
NO_RESPONSE = Object.new.freeze
|
|
25
|
+
|
|
21
26
|
extend self
|
|
22
27
|
|
|
23
28
|
def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
|
|
@@ -103,6 +108,7 @@ module JsonRpcHandler
|
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
result = method.call(params)
|
|
111
|
+
return if result.equal?(NO_RESPONSE)
|
|
106
112
|
|
|
107
113
|
success_response(id: id, result: result)
|
|
108
114
|
rescue MCP::Server::RequestHandlerError => e
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cancelled_error"
|
|
4
|
+
|
|
5
|
+
module MCP
|
|
6
|
+
class Cancellation
|
|
7
|
+
attr_reader :reason, :request_id
|
|
8
|
+
|
|
9
|
+
def initialize(request_id: nil)
|
|
10
|
+
@request_id = request_id
|
|
11
|
+
@reason = nil
|
|
12
|
+
@cancelled = false
|
|
13
|
+
@callbacks = []
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cancelled?
|
|
18
|
+
@mutex.synchronize { @cancelled }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cancel(reason: nil)
|
|
22
|
+
callbacks = @mutex.synchronize do
|
|
23
|
+
return false if @cancelled
|
|
24
|
+
|
|
25
|
+
@cancelled = true
|
|
26
|
+
@reason = reason
|
|
27
|
+
@callbacks.tap { @callbacks = [] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
callbacks.each do |callback|
|
|
31
|
+
callback.call(reason)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
MCP.configuration.exception_reporter.call(e, { error: "Cancellation callback failed" })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Registers a callback invoked synchronously on the first `cancel` call.
|
|
40
|
+
# If already cancelled, fires immediately.
|
|
41
|
+
#
|
|
42
|
+
# Returns the block itself as a handle that can be passed to `off_cancel`
|
|
43
|
+
# to deregister it (e.g. when a nested request completes normally and the
|
|
44
|
+
# hook should not fire on a later parent cancellation).
|
|
45
|
+
def on_cancel(&block)
|
|
46
|
+
fire_now = false
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
if @cancelled
|
|
49
|
+
fire_now = true
|
|
50
|
+
else
|
|
51
|
+
@callbacks << block
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
block.call(@reason) if fire_now
|
|
56
|
+
block
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Removes a previously-registered `on_cancel` callback. Returns `true`
|
|
60
|
+
# if the callback was still pending (i.e. had not yet fired), `false`
|
|
61
|
+
# otherwise. Safe to call with `nil`.
|
|
62
|
+
def off_cancel(handle)
|
|
63
|
+
return false unless handle
|
|
64
|
+
|
|
65
|
+
@mutex.synchronize { !@callbacks.delete(handle).nil? }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def raise_if_cancelled!
|
|
69
|
+
raise CancelledError.new(request_id: @request_id, reason: @reason) if cancelled?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class CancelledError < StandardError
|
|
5
|
+
attr_reader :request_id, :reason
|
|
6
|
+
|
|
7
|
+
def initialize(message = "Request was cancelled", request_id: nil, reason: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@request_id = request_id
|
|
10
|
+
@reason = reason
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/mcp/client/http.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "../../json_rpc_handler"
|
|
5
|
+
require_relative "../configuration"
|
|
3
6
|
require_relative "../methods"
|
|
7
|
+
require_relative "../version"
|
|
4
8
|
|
|
5
9
|
module MCP
|
|
6
10
|
class Client
|
|
@@ -13,7 +17,7 @@ module MCP
|
|
|
13
17
|
SESSION_ID_HEADER = "Mcp-Session-Id"
|
|
14
18
|
PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version"
|
|
15
19
|
|
|
16
|
-
attr_reader :url, :session_id, :protocol_version
|
|
20
|
+
attr_reader :url, :session_id, :protocol_version, :server_info
|
|
17
21
|
|
|
18
22
|
def initialize(url:, headers: {}, &block)
|
|
19
23
|
@url = url
|
|
@@ -21,6 +25,94 @@ module MCP
|
|
|
21
25
|
@faraday_customizer = block
|
|
22
26
|
@session_id = nil
|
|
23
27
|
@protocol_version = nil
|
|
28
|
+
@server_info = nil
|
|
29
|
+
@connected = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Performs the MCP `initialize` handshake: sends an `initialize` request
|
|
33
|
+
# followed by the required `notifications/initialized` notification. The
|
|
34
|
+
# server's `InitializeResult` (protocol version, capabilities, server
|
|
35
|
+
# info, instructions) is cached on the transport and returned.
|
|
36
|
+
#
|
|
37
|
+
# Idempotent: a second call returns the cached `InitializeResult` without
|
|
38
|
+
# contacting the server. After `close`, state is cleared and `connect`
|
|
39
|
+
# will handshake again.
|
|
40
|
+
#
|
|
41
|
+
# @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
|
|
42
|
+
# Defaults to `{ name: "mcp-ruby-client", version: MCP::VERSION }`.
|
|
43
|
+
# @param protocol_version [String, nil] Protocol version to offer. Defaults
|
|
44
|
+
# to `MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION`.
|
|
45
|
+
# @param capabilities [Hash] Capabilities advertised by the client. Defaults to `{}`.
|
|
46
|
+
# @return [Hash] The server's `InitializeResult`.
|
|
47
|
+
# @raise [RequestHandlerError] If the server responds with a JSON-RPC error
|
|
48
|
+
# or a malformed result.
|
|
49
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
|
|
50
|
+
def connect(client_info: nil, protocol_version: nil, capabilities: {})
|
|
51
|
+
return @server_info if connected?
|
|
52
|
+
|
|
53
|
+
client_info ||= { name: "mcp-ruby-client", version: MCP::VERSION }
|
|
54
|
+
protocol_version ||= MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION
|
|
55
|
+
|
|
56
|
+
response = send_request(request: {
|
|
57
|
+
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
58
|
+
id: SecureRandom.uuid,
|
|
59
|
+
method: MCP::Methods::INITIALIZE,
|
|
60
|
+
params: {
|
|
61
|
+
protocolVersion: protocol_version,
|
|
62
|
+
capabilities: capabilities,
|
|
63
|
+
clientInfo: client_info,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if response.is_a?(Hash) && response.key?("error")
|
|
68
|
+
clear_session
|
|
69
|
+
error = response["error"]
|
|
70
|
+
raise RequestHandlerError.new(
|
|
71
|
+
"Server initialization failed: #{error["message"]}",
|
|
72
|
+
{ method: MCP::Methods::INITIALIZE },
|
|
73
|
+
error_type: :internal_error,
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
unless response.is_a?(Hash) && response["result"].is_a?(Hash)
|
|
78
|
+
clear_session
|
|
79
|
+
raise RequestHandlerError.new(
|
|
80
|
+
"Server initialization failed: missing result in response",
|
|
81
|
+
{ method: MCP::Methods::INITIALIZE },
|
|
82
|
+
error_type: :internal_error,
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@server_info = response["result"]
|
|
87
|
+
negotiated_protocol_version = @server_info["protocolVersion"]
|
|
88
|
+
unless MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(negotiated_protocol_version)
|
|
89
|
+
clear_session
|
|
90
|
+
raise RequestHandlerError.new(
|
|
91
|
+
"Server initialization failed: unsupported protocol version #{negotiated_protocol_version.inspect}",
|
|
92
|
+
{ method: MCP::Methods::INITIALIZE },
|
|
93
|
+
error_type: :internal_error,
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
send_request(request: {
|
|
99
|
+
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
100
|
+
method: MCP::Methods::NOTIFICATIONS_INITIALIZED,
|
|
101
|
+
})
|
|
102
|
+
rescue StandardError
|
|
103
|
+
clear_session
|
|
104
|
+
raise
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
@connected = true
|
|
108
|
+
@server_info
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns true once `connect` has completed the full handshake
|
|
112
|
+
# (`initialize` response received and `notifications/initialized` sent).
|
|
113
|
+
# Returns false before the first handshake and after `close`.
|
|
114
|
+
def connected?
|
|
115
|
+
@connected
|
|
24
116
|
end
|
|
25
117
|
|
|
26
118
|
# Sends a JSON-RPC request and returns the parsed response body.
|
|
@@ -105,7 +197,10 @@ module MCP
|
|
|
105
197
|
# session state is cleared either way.
|
|
106
198
|
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
|
|
107
199
|
def close
|
|
108
|
-
|
|
200
|
+
unless @session_id
|
|
201
|
+
clear_session
|
|
202
|
+
return
|
|
203
|
+
end
|
|
109
204
|
|
|
110
205
|
begin
|
|
111
206
|
client.delete("", nil, session_headers)
|
|
@@ -159,6 +254,8 @@ module MCP
|
|
|
159
254
|
def clear_session
|
|
160
255
|
@session_id = nil
|
|
161
256
|
@protocol_version = nil
|
|
257
|
+
@server_info = nil
|
|
258
|
+
@connected = false
|
|
162
259
|
end
|
|
163
260
|
|
|
164
261
|
def require_faraday!
|
data/lib/mcp/client/stdio.rb
CHANGED
|
@@ -19,7 +19,7 @@ module MCP
|
|
|
19
19
|
CLOSE_TIMEOUT = 2
|
|
20
20
|
STDERR_READ_SIZE = 4096
|
|
21
21
|
|
|
22
|
-
attr_reader :command, :args, :env
|
|
22
|
+
attr_reader :command, :args, :env, :server_info
|
|
23
23
|
|
|
24
24
|
def initialize(command:, args: [], env: nil, read_timeout: nil)
|
|
25
25
|
@command = command
|
|
@@ -33,11 +33,108 @@ module MCP
|
|
|
33
33
|
@stderr_thread = nil
|
|
34
34
|
@started = false
|
|
35
35
|
@initialized = false
|
|
36
|
+
@server_info = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Performs the MCP `initialize` handshake: sends an `initialize` request
|
|
40
|
+
# followed by the required `notifications/initialized` notification. The
|
|
41
|
+
# server's `InitializeResult` (protocol version, capabilities, server
|
|
42
|
+
# info, instructions) is cached on the transport and returned.
|
|
43
|
+
#
|
|
44
|
+
# Idempotent: a second call returns the cached `InitializeResult` without
|
|
45
|
+
# contacting the server. After `close`, state is cleared and `connect`
|
|
46
|
+
# will handshake again. Spawns the subprocess via `start` if it has not
|
|
47
|
+
# been started yet.
|
|
48
|
+
#
|
|
49
|
+
# @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
|
|
50
|
+
# Defaults to `{ name: "mcp-ruby-client", version: MCP::VERSION }`.
|
|
51
|
+
# @param protocol_version [String, nil] Protocol version to offer. Defaults
|
|
52
|
+
# to `MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION`.
|
|
53
|
+
# @param capabilities [Hash] Capabilities advertised by the client. Defaults to `{}`.
|
|
54
|
+
# @return [Hash] The server's `InitializeResult`.
|
|
55
|
+
# @raise [RequestHandlerError] If the server responds with a JSON-RPC error,
|
|
56
|
+
# a malformed result, or an unsupported protocol version.
|
|
57
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
|
|
58
|
+
def connect(client_info: nil, protocol_version: nil, capabilities: {})
|
|
59
|
+
return @server_info if @initialized
|
|
60
|
+
|
|
61
|
+
start unless @started
|
|
62
|
+
|
|
63
|
+
client_info ||= { name: "mcp-ruby-client", version: MCP::VERSION }
|
|
64
|
+
protocol_version ||= MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION
|
|
65
|
+
|
|
66
|
+
init_request = {
|
|
67
|
+
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
68
|
+
id: SecureRandom.uuid,
|
|
69
|
+
method: MCP::Methods::INITIALIZE,
|
|
70
|
+
params: {
|
|
71
|
+
protocolVersion: protocol_version,
|
|
72
|
+
capabilities: capabilities,
|
|
73
|
+
clientInfo: client_info,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
write_message(init_request)
|
|
78
|
+
response = read_response(init_request)
|
|
79
|
+
|
|
80
|
+
if response.key?("error")
|
|
81
|
+
error = response["error"]
|
|
82
|
+
raise RequestHandlerError.new(
|
|
83
|
+
"Server initialization failed: #{error["message"]}",
|
|
84
|
+
{ method: MCP::Methods::INITIALIZE },
|
|
85
|
+
error_type: :internal_error,
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless response["result"].is_a?(Hash)
|
|
90
|
+
raise RequestHandlerError.new(
|
|
91
|
+
"Server initialization failed: missing result in response",
|
|
92
|
+
{ method: MCP::Methods::INITIALIZE },
|
|
93
|
+
error_type: :internal_error,
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@server_info = response["result"]
|
|
98
|
+
|
|
99
|
+
negotiated_protocol_version = @server_info["protocolVersion"]
|
|
100
|
+
unless MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(negotiated_protocol_version)
|
|
101
|
+
# Per spec, if the client does not support the server's returned protocol version,
|
|
102
|
+
# the client SHOULD disconnect. Roll back the cached `InitializeResult` before
|
|
103
|
+
# raising so a retry starts without a stale `server_info`.
|
|
104
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation
|
|
105
|
+
@server_info = nil
|
|
106
|
+
raise RequestHandlerError.new(
|
|
107
|
+
"Server initialization failed: unsupported protocol version #{negotiated_protocol_version.inspect}",
|
|
108
|
+
{ method: MCP::Methods::INITIALIZE },
|
|
109
|
+
error_type: :internal_error,
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
begin
|
|
114
|
+
notification = {
|
|
115
|
+
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
116
|
+
method: MCP::Methods::NOTIFICATIONS_INITIALIZED,
|
|
117
|
+
}
|
|
118
|
+
write_message(notification)
|
|
119
|
+
rescue StandardError
|
|
120
|
+
@server_info = nil
|
|
121
|
+
raise
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@initialized = true
|
|
125
|
+
@server_info
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns true once `connect` (or the implicit handshake on the first
|
|
129
|
+
# `send_request`) has completed. Returns false before the handshake
|
|
130
|
+
# and after `close`.
|
|
131
|
+
def connected?
|
|
132
|
+
@initialized
|
|
36
133
|
end
|
|
37
134
|
|
|
38
135
|
def send_request(request:)
|
|
39
136
|
start unless @started
|
|
40
|
-
|
|
137
|
+
connect unless @initialized
|
|
41
138
|
|
|
42
139
|
write_message(request)
|
|
43
140
|
read_response(request)
|
|
@@ -98,57 +195,11 @@ module MCP
|
|
|
98
195
|
@stderr_thread.join(CLOSE_TIMEOUT)
|
|
99
196
|
@started = false
|
|
100
197
|
@initialized = false
|
|
198
|
+
@server_info = nil
|
|
101
199
|
end
|
|
102
200
|
|
|
103
201
|
private
|
|
104
202
|
|
|
105
|
-
# The client MUST send a protocol version it supports. This SHOULD be the latest version.
|
|
106
|
-
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation
|
|
107
|
-
#
|
|
108
|
-
# Always sends `LATEST_STABLE_PROTOCOL_VERSION`, matching the Python and TypeScript SDKs:
|
|
109
|
-
# https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/session.py#L175
|
|
110
|
-
# https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.27.1/src/client/index.ts#L495
|
|
111
|
-
def initialize_session
|
|
112
|
-
init_request = {
|
|
113
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
114
|
-
id: SecureRandom.uuid,
|
|
115
|
-
method: MCP::Methods::INITIALIZE,
|
|
116
|
-
params: {
|
|
117
|
-
protocolVersion: MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION,
|
|
118
|
-
capabilities: {},
|
|
119
|
-
clientInfo: { name: "mcp-ruby-client", version: MCP::VERSION },
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
write_message(init_request)
|
|
124
|
-
response = read_response(init_request)
|
|
125
|
-
|
|
126
|
-
if response.key?("error")
|
|
127
|
-
error = response["error"]
|
|
128
|
-
raise RequestHandlerError.new(
|
|
129
|
-
"Server initialization failed: #{error["message"]}",
|
|
130
|
-
{ method: MCP::Methods::INITIALIZE },
|
|
131
|
-
error_type: :internal_error,
|
|
132
|
-
)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
unless response.key?("result")
|
|
136
|
-
raise RequestHandlerError.new(
|
|
137
|
-
"Server initialization failed: missing result in response",
|
|
138
|
-
{ method: MCP::Methods::INITIALIZE },
|
|
139
|
-
error_type: :internal_error,
|
|
140
|
-
)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
notification = {
|
|
144
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
145
|
-
method: MCP::Methods::NOTIFICATIONS_INITIALIZED,
|
|
146
|
-
}
|
|
147
|
-
write_message(notification)
|
|
148
|
-
|
|
149
|
-
@initialized = true
|
|
150
|
-
end
|
|
151
|
-
|
|
152
203
|
def write_message(message)
|
|
153
204
|
ensure_running!
|
|
154
205
|
json = JSON.generate(message)
|
data/lib/mcp/client.rb
CHANGED
|
@@ -59,6 +59,46 @@ module MCP
|
|
|
59
59
|
# So keeping it public
|
|
60
60
|
attr_reader :transport
|
|
61
61
|
|
|
62
|
+
# The server's `InitializeResult` (protocol version, capabilities, server info,
|
|
63
|
+
# instructions), as reported by the transport after a successful `connect`.
|
|
64
|
+
# Returns `nil` before `connect`, after `close`, or when the transport does
|
|
65
|
+
# not expose a cached handshake result.
|
|
66
|
+
def server_info
|
|
67
|
+
transport.server_info if transport.respond_to?(:server_info)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Performs the MCP `initialize` handshake by delegating to the transport
|
|
71
|
+
# (e.g. `MCP::Client::HTTP`, `MCP::Client::Stdio`). Returns the server's
|
|
72
|
+
# `InitializeResult`.
|
|
73
|
+
#
|
|
74
|
+
# When the transport does not respond to `:connect`, this is a no-op and
|
|
75
|
+
# returns `nil`.
|
|
76
|
+
#
|
|
77
|
+
# @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
|
|
78
|
+
# @param protocol_version [String, nil] Protocol version to offer.
|
|
79
|
+
# @param capabilities [Hash] Capabilities advertised by the client.
|
|
80
|
+
# @return [Hash, nil] The server's `InitializeResult`, or `nil` when the transport
|
|
81
|
+
# does not expose an explicit handshake.
|
|
82
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
|
|
83
|
+
def connect(client_info: nil, protocol_version: nil, capabilities: {})
|
|
84
|
+
return unless transport.respond_to?(:connect)
|
|
85
|
+
|
|
86
|
+
transport.connect(
|
|
87
|
+
client_info: client_info,
|
|
88
|
+
protocol_version: protocol_version,
|
|
89
|
+
capabilities: capabilities,
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns true once `connect` has completed the handshake on the underlying
|
|
94
|
+
# transport. Transports that do not expose connection state are assumed
|
|
95
|
+
# connected and return `true`.
|
|
96
|
+
def connected?
|
|
97
|
+
return transport.connected? if transport.respond_to?(:connected?)
|
|
98
|
+
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
62
102
|
# Returns a single page of tools from the server.
|
|
63
103
|
#
|
|
64
104
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
@@ -54,6 +54,13 @@ module MCP
|
|
|
54
54
|
false
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# NOTE: This signature deliberately matches the abstract `Transport#send_request` contract
|
|
58
|
+
# (`method, params = nil`) without the cancellation kwargs that `StreamableHTTPTransport#send_request` accepts.
|
|
59
|
+
# On Ruby 2.7 the project's supported minimum a method that mixes a positional `params` Hash with
|
|
60
|
+
# explicit keyword arguments cannot be called as `send_request(method, { ... })` - the trailing Hash would be
|
|
61
|
+
# auto-promoted to keyword arguments. Stdio is single-threaded and blocks on `$stdin.gets`, so nested-request
|
|
62
|
+
# cancellation has very limited value here regardless; servers that need cancellation propagation for nested
|
|
63
|
+
# server-to-client requests should use `StreamableHTTPTransport`.
|
|
57
64
|
def send_request(method, params = nil)
|
|
58
65
|
request_id = generate_request_id
|
|
59
66
|
request = { jsonrpc: "2.0", id: request_id, method: method }
|
|
@@ -175,7 +175,7 @@ module MCP
|
|
|
175
175
|
# sends the request via SSE stream, then blocks on `queue.pop`.
|
|
176
176
|
# When the client POSTs a response, `handle_response` matches it by `request_id`
|
|
177
177
|
# and pushes the result onto the queue, unblocking this thread.
|
|
178
|
-
def send_request(method, params = nil, session_id: nil, related_request_id: nil)
|
|
178
|
+
def send_request(method, params = nil, session_id: nil, related_request_id: nil, parent_cancellation: nil, server_session: nil)
|
|
179
179
|
if @stateless
|
|
180
180
|
raise "Stateless mode does not support server-to-client requests."
|
|
181
181
|
end
|
|
@@ -190,6 +190,7 @@ module MCP
|
|
|
190
190
|
|
|
191
191
|
request_id = generate_request_id
|
|
192
192
|
queue = Queue.new
|
|
193
|
+
cancel_hook = nil
|
|
193
194
|
|
|
194
195
|
request = { jsonrpc: "2.0", id: request_id, method: method }
|
|
195
196
|
request[:params] = params if params
|
|
@@ -229,6 +230,16 @@ module MCP
|
|
|
229
230
|
raise "No active stream for #{method} request."
|
|
230
231
|
end
|
|
231
232
|
|
|
233
|
+
if parent_cancellation && server_session
|
|
234
|
+
cancel_hook = parent_cancellation.on_cancel do |reason|
|
|
235
|
+
server_session.send_peer_cancellation(
|
|
236
|
+
nested_request_id: request_id,
|
|
237
|
+
related_request_id: related_request_id,
|
|
238
|
+
reason: reason,
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
232
243
|
response = queue.pop
|
|
233
244
|
|
|
234
245
|
if response.is_a?(Hash) && response.key?(:error)
|
|
@@ -239,8 +250,18 @@ module MCP
|
|
|
239
250
|
raise "SSE session closed while waiting for #{method} response."
|
|
240
251
|
end
|
|
241
252
|
|
|
253
|
+
if response == :cancelled
|
|
254
|
+
reason = @mutex.synchronize { @pending_responses.dig(request_id, :cancel_reason) }
|
|
255
|
+
raise MCP::CancelledError.new(
|
|
256
|
+
"#{method} request was cancelled",
|
|
257
|
+
request_id: request_id,
|
|
258
|
+
reason: reason,
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
242
262
|
response
|
|
243
263
|
ensure
|
|
264
|
+
parent_cancellation.off_cancel(cancel_hook) if cancel_hook
|
|
244
265
|
if request_id
|
|
245
266
|
@mutex.synchronize do
|
|
246
267
|
@pending_responses.delete(request_id)
|
|
@@ -248,6 +269,24 @@ module MCP
|
|
|
248
269
|
end
|
|
249
270
|
end
|
|
250
271
|
|
|
272
|
+
# Unblocks a `send_request` awaiting a response when the peer is being cancelled.
|
|
273
|
+
# The waiting thread will see `:cancelled` on its queue and raise `MCP::CancelledError`.
|
|
274
|
+
#
|
|
275
|
+
# Race note: this is first-writer-wins on the pending-response queue. If a real response
|
|
276
|
+
# has already been pushed (client responded before the cancel hook fired), that response
|
|
277
|
+
# wins and `:cancelled` is enqueued behind it but never read - `send_request` returns
|
|
278
|
+
# the real response and deletes the pending entry in its `ensure` block. Conversely,
|
|
279
|
+
# if `:cancelled` arrives first, any later client response is silently dropped in `handle_response`
|
|
280
|
+
# because the pending entry has been removed.
|
|
281
|
+
def cancel_pending_request(request_id, reason: nil)
|
|
282
|
+
@mutex.synchronize do
|
|
283
|
+
if (pending = @pending_responses[request_id])
|
|
284
|
+
pending[:cancel_reason] = reason
|
|
285
|
+
pending[:queue].push(:cancelled)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
251
290
|
private
|
|
252
291
|
|
|
253
292
|
def start_reaper_thread
|
|
@@ -309,6 +348,7 @@ module MCP
|
|
|
309
348
|
return missing_session_id_response if !@stateless && !session_id
|
|
310
349
|
|
|
311
350
|
if notification?(body)
|
|
351
|
+
dispatch_notification(body_string, session_id)
|
|
312
352
|
handle_accepted
|
|
313
353
|
elsif response?(body)
|
|
314
354
|
return session_not_found_response if !@stateless && !session_exists?(session_id)
|
|
@@ -459,6 +499,22 @@ module MCP
|
|
|
459
499
|
!body[:id] && !!body[:method]
|
|
460
500
|
end
|
|
461
501
|
|
|
502
|
+
# Dispatches a client-originated notification (e.g. `notifications/cancelled`,
|
|
503
|
+
# `notifications/initialized`) through the server so it can update session state.
|
|
504
|
+
def dispatch_notification(body_string, session_id)
|
|
505
|
+
server_session = nil
|
|
506
|
+
if session_id && !@stateless
|
|
507
|
+
@mutex.synchronize do
|
|
508
|
+
session = @sessions[session_id]
|
|
509
|
+
server_session = session[:server_session] if session
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
dispatch_handle_json(body_string, server_session)
|
|
514
|
+
rescue => e
|
|
515
|
+
MCP.configuration.exception_reporter.call(e, { error: "Failed to dispatch notification" })
|
|
516
|
+
end
|
|
517
|
+
|
|
462
518
|
def response?(body)
|
|
463
519
|
!!body[:id] && !body[:method]
|
|
464
520
|
end
|
|
@@ -536,6 +592,12 @@ module MCP
|
|
|
536
592
|
handle_request_with_sse_response(body_string, session_id, server_session, related_request_id: related_request_id)
|
|
537
593
|
else
|
|
538
594
|
response = dispatch_handle_json(body_string, server_session)
|
|
595
|
+
|
|
596
|
+
# `Server#handle_json` returns `nil` when cancellation has suppressed the JSON-RPC response per spec.
|
|
597
|
+
# Mirror the notification path and ack with 202 instead of returning a 200 with a `nil` Rack body,
|
|
598
|
+
# which would produce an empty body the client cannot parse as JSON.
|
|
599
|
+
return handle_accepted if response.nil?
|
|
600
|
+
|
|
539
601
|
[200, { "Content-Type" => "application/json" }, [response]]
|
|
540
602
|
end
|
|
541
603
|
end
|
data/lib/mcp/server.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../json_rpc_handler"
|
|
4
|
+
require_relative "cancellation"
|
|
5
|
+
require_relative "cancelled_error"
|
|
4
6
|
require_relative "instrumentation"
|
|
5
7
|
require_relative "methods"
|
|
6
8
|
require_relative "logging_message_notification"
|
|
@@ -384,6 +386,13 @@ module MCP
|
|
|
384
386
|
end
|
|
385
387
|
|
|
386
388
|
def handle_request(request, method, session: nil, related_request_id: nil)
|
|
389
|
+
# `notifications/cancelled` is dispatched directly: it is a notification (no JSON-RPC id)
|
|
390
|
+
# and intentionally bypasses the `@handlers` lookup, capability check, in-flight registry,
|
|
391
|
+
# and rescue blocks below.
|
|
392
|
+
if method == Methods::NOTIFICATIONS_CANCELLED
|
|
393
|
+
return ->(params) { handle_cancelled_notification(params, session: session) }
|
|
394
|
+
end
|
|
395
|
+
|
|
387
396
|
handler = @handlers[method]
|
|
388
397
|
unless handler
|
|
389
398
|
instrument_call("unsupported_method", server_context: { request: request }) do
|
|
@@ -395,6 +404,12 @@ module MCP
|
|
|
395
404
|
|
|
396
405
|
Methods.ensure_capability!(method, capabilities)
|
|
397
406
|
|
|
407
|
+
# `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2),
|
|
408
|
+
# so do not track it in the in-flight registry.
|
|
409
|
+
cancellation = if related_request_id && method != Methods::INITIALIZE
|
|
410
|
+
session&.register_in_flight(related_request_id)
|
|
411
|
+
end
|
|
412
|
+
|
|
398
413
|
->(params) {
|
|
399
414
|
reported_exception = nil
|
|
400
415
|
instrument_call(
|
|
@@ -406,23 +421,33 @@ module MCP
|
|
|
406
421
|
when Methods::INITIALIZE
|
|
407
422
|
init(params, session: session)
|
|
408
423
|
when Methods::RESOURCES_READ
|
|
409
|
-
{ contents:
|
|
424
|
+
{ contents: read_resource_contents(params, session: session, related_request_id: related_request_id, cancellation: cancellation) }
|
|
410
425
|
when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
|
|
411
|
-
@handlers[method]
|
|
426
|
+
dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation)
|
|
412
427
|
{}
|
|
413
428
|
when Methods::TOOLS_CALL
|
|
414
|
-
call_tool(params, session: session, related_request_id: related_request_id)
|
|
429
|
+
call_tool(params, session: session, related_request_id: related_request_id, cancellation: cancellation)
|
|
430
|
+
when Methods::PROMPTS_GET
|
|
431
|
+
get_prompt(params, session: session, related_request_id: related_request_id, cancellation: cancellation)
|
|
415
432
|
when Methods::COMPLETION_COMPLETE
|
|
416
|
-
complete(params)
|
|
433
|
+
complete(params, session: session, related_request_id: related_request_id, cancellation: cancellation)
|
|
417
434
|
when Methods::LOGGING_SET_LEVEL
|
|
418
435
|
configure_logging_level(params, session: session)
|
|
419
436
|
else
|
|
420
|
-
@handlers[method]
|
|
437
|
+
dispatch_optional_context_handler(@handlers[method], params, session: session, related_request_id: related_request_id, cancellation: cancellation)
|
|
421
438
|
end
|
|
422
439
|
client = session&.client || @client
|
|
423
440
|
add_instrumentation_data(client: client) if client
|
|
424
441
|
|
|
442
|
+
if cancellation&.cancelled?
|
|
443
|
+
add_instrumentation_data(cancelled: true, cancellation_reason: cancellation.reason)
|
|
444
|
+
next JsonRpcHandler::NO_RESPONSE
|
|
445
|
+
end
|
|
446
|
+
|
|
425
447
|
result
|
|
448
|
+
rescue CancelledError => e
|
|
449
|
+
add_instrumentation_data(cancelled: true, cancellation_reason: e.reason)
|
|
450
|
+
next JsonRpcHandler::NO_RESPONSE
|
|
426
451
|
rescue RequestHandlerError => e
|
|
427
452
|
report_exception(e.original_error || e, { request: request })
|
|
428
453
|
add_instrumentation_data(error: e.error_type)
|
|
@@ -434,10 +459,23 @@ module MCP
|
|
|
434
459
|
wrapped = RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
|
|
435
460
|
reported_exception = wrapped
|
|
436
461
|
raise wrapped
|
|
462
|
+
ensure
|
|
463
|
+
session&.unregister_in_flight(related_request_id) if related_request_id
|
|
437
464
|
end
|
|
438
465
|
}
|
|
439
466
|
end
|
|
440
467
|
|
|
468
|
+
def handle_cancelled_notification(params, session: nil)
|
|
469
|
+
return unless session
|
|
470
|
+
return unless params.is_a?(Hash)
|
|
471
|
+
|
|
472
|
+
request_id = params[:requestId] || params["requestId"]
|
|
473
|
+
return if request_id.nil?
|
|
474
|
+
|
|
475
|
+
reason = params[:reason] || params["reason"]
|
|
476
|
+
session.cancel_incoming(request_id: request_id, reason: reason)
|
|
477
|
+
end
|
|
478
|
+
|
|
441
479
|
def default_capabilities
|
|
442
480
|
{
|
|
443
481
|
tools: { listChanged: true },
|
|
@@ -516,7 +554,7 @@ module MCP
|
|
|
516
554
|
{ tools: page[:items], nextCursor: page[:next_cursor] }.compact
|
|
517
555
|
end
|
|
518
556
|
|
|
519
|
-
def call_tool(request, session: nil, related_request_id: nil)
|
|
557
|
+
def call_tool(request, session: nil, related_request_id: nil, cancellation: nil)
|
|
520
558
|
tool_name = request[:name]
|
|
521
559
|
|
|
522
560
|
tool = tools[tool_name]
|
|
@@ -533,7 +571,7 @@ module MCP
|
|
|
533
571
|
add_instrumentation_data(error: :missing_required_arguments)
|
|
534
572
|
|
|
535
573
|
missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
|
|
536
|
-
|
|
574
|
+
return error_tool_response("Missing required arguments: #{missing}")
|
|
537
575
|
end
|
|
538
576
|
|
|
539
577
|
if configuration.validate_tool_call_arguments && tool.input_schema
|
|
@@ -542,14 +580,18 @@ module MCP
|
|
|
542
580
|
rescue Tool::InputSchema::ValidationError => e
|
|
543
581
|
add_instrumentation_data(error: :invalid_schema)
|
|
544
582
|
|
|
545
|
-
|
|
583
|
+
return error_tool_response(e.message)
|
|
546
584
|
end
|
|
547
585
|
end
|
|
548
586
|
|
|
549
587
|
progress_token = request.dig(:_meta, :progressToken)
|
|
550
588
|
|
|
551
|
-
call_tool_with_args(
|
|
552
|
-
|
|
589
|
+
call_tool_with_args(
|
|
590
|
+
tool, arguments, server_context_with_meta(request), progress_token: progress_token, session: session, related_request_id: related_request_id, cancellation: cancellation
|
|
591
|
+
)
|
|
592
|
+
rescue RequestHandlerError, CancelledError
|
|
593
|
+
# CancelledError is intentionally not wrapped so `handle_request` can turn it into
|
|
594
|
+
# `JsonRpcHandler::NO_RESPONSE` per the MCP cancellation spec.
|
|
553
595
|
raise
|
|
554
596
|
rescue => e
|
|
555
597
|
raise RequestHandlerError.new(
|
|
@@ -566,7 +608,7 @@ module MCP
|
|
|
566
608
|
{ prompts: page[:items], nextCursor: page[:next_cursor] }.compact
|
|
567
609
|
end
|
|
568
610
|
|
|
569
|
-
def get_prompt(request)
|
|
611
|
+
def get_prompt(request, session: nil, related_request_id: nil, cancellation: nil)
|
|
570
612
|
prompt_name = request[:name]
|
|
571
613
|
prompt = @prompts[prompt_name]
|
|
572
614
|
unless prompt
|
|
@@ -579,7 +621,14 @@ module MCP
|
|
|
579
621
|
prompt_args = request[:arguments]
|
|
580
622
|
prompt.validate_arguments!(prompt_args)
|
|
581
623
|
|
|
582
|
-
|
|
624
|
+
server_context = build_server_context(
|
|
625
|
+
request: request,
|
|
626
|
+
session: session,
|
|
627
|
+
related_request_id: related_request_id,
|
|
628
|
+
cancellation: cancellation,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
call_prompt_template_with_args(prompt, prompt_args, server_context)
|
|
583
632
|
end
|
|
584
633
|
|
|
585
634
|
def list_resources(request)
|
|
@@ -600,14 +649,82 @@ module MCP
|
|
|
600
649
|
{ resourceTemplates: page[:items], nextCursor: page[:next_cursor] }.compact
|
|
601
650
|
end
|
|
602
651
|
|
|
603
|
-
def complete(params)
|
|
652
|
+
def complete(params, session: nil, related_request_id: nil, cancellation: nil)
|
|
604
653
|
validate_completion_params!(params)
|
|
605
654
|
|
|
606
|
-
result =
|
|
655
|
+
result = dispatch_optional_context_handler(
|
|
656
|
+
@handlers[Methods::COMPLETION_COMPLETE],
|
|
657
|
+
params,
|
|
658
|
+
session: session,
|
|
659
|
+
related_request_id: related_request_id,
|
|
660
|
+
cancellation: cancellation,
|
|
661
|
+
)
|
|
607
662
|
|
|
608
663
|
normalize_completion_result(result)
|
|
609
664
|
end
|
|
610
665
|
|
|
666
|
+
# Invokes `resources/read` via the registered handler. If the handler block opts in to `server_context:`,
|
|
667
|
+
# pass an `MCP::ServerContext` so the handler can observe cancellation via `server_context.cancelled?` or
|
|
668
|
+
# `server_context.raise_if_cancelled!`.
|
|
669
|
+
def read_resource_contents(request, session: nil, related_request_id: nil, cancellation: nil)
|
|
670
|
+
dispatch_optional_context_handler(
|
|
671
|
+
@handlers[Methods::RESOURCES_READ],
|
|
672
|
+
request,
|
|
673
|
+
session: session,
|
|
674
|
+
related_request_id: related_request_id,
|
|
675
|
+
cancellation: cancellation,
|
|
676
|
+
)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Opt-in `server_context:` dispatch for block-based handlers registered via `resources_read_handler`,
|
|
680
|
+
# `completion_handler`, `resources_subscribe_handler`, `resources_unsubscribe_handler`, or `define_custom_method`.
|
|
681
|
+
# Existing handlers that only accept `params` are called unchanged; handlers that declare a `server_context:`
|
|
682
|
+
# keyword receive an `MCP::ServerContext` wrapping the raw server context with cancellation plumbing.
|
|
683
|
+
def dispatch_optional_context_handler(handler, params, session: nil, related_request_id: nil, cancellation: nil)
|
|
684
|
+
return handler.call(params) unless handler_declares_server_context?(handler)
|
|
685
|
+
|
|
686
|
+
server_context = build_server_context(
|
|
687
|
+
request: params,
|
|
688
|
+
session: session,
|
|
689
|
+
related_request_id: related_request_id,
|
|
690
|
+
cancellation: cancellation,
|
|
691
|
+
)
|
|
692
|
+
handler.call(params, server_context: server_context)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Stricter than `accepts_server_context?`: requires `server_context` to appear as a named keyword parameter
|
|
696
|
+
# (`:key` optional, `:keyreq` required). Positional parameters named `server_context` (`:req` / `:opt`) are NOT
|
|
697
|
+
# treated as opt-in - otherwise `handler.call(params, server_context: ctx)` would pass the `{server_context: ctx}`
|
|
698
|
+
# Hash as the handler's second positional argument, which is never what the user meant.
|
|
699
|
+
#
|
|
700
|
+
# `**kwargs`-only signatures (`:keyrest` without a named `server_context`) are also not opt-in here,
|
|
701
|
+
# because the dispatch site passes a positional `params`, and a `**kwargs`-only block cannot accept
|
|
702
|
+
# that positional argument (lambdas/methods raise `ArgumentError`; non-lambda procs silently drop `params`).
|
|
703
|
+
# Tool handlers intentionally allow `**kwargs` opt-in via `accepts_server_context?` because they are invoked
|
|
704
|
+
# via `tool.call(**args, server_context: …)` without a positional argument.
|
|
705
|
+
def handler_declares_server_context?(handler)
|
|
706
|
+
return false unless handler.respond_to?(:parameters)
|
|
707
|
+
|
|
708
|
+
handler.parameters.any? do |type, name|
|
|
709
|
+
name == :server_context && (type == :key || type == :keyreq)
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# Builds an `MCP::ServerContext` used to give a handler access to session-scoped helpers
|
|
714
|
+
# (progress, cancellation, nested server-to-client requests).
|
|
715
|
+
def build_server_context(request:, session:, related_request_id:, cancellation:)
|
|
716
|
+
meta_source = request.is_a?(Hash) ? request : {}
|
|
717
|
+
progress_token = meta_source.dig(:_meta, :progressToken)
|
|
718
|
+
progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
|
|
719
|
+
ServerContext.new(
|
|
720
|
+
server_context_with_meta(meta_source),
|
|
721
|
+
progress: progress,
|
|
722
|
+
notification_target: session,
|
|
723
|
+
related_request_id: related_request_id,
|
|
724
|
+
cancellation: cancellation,
|
|
725
|
+
)
|
|
726
|
+
end
|
|
727
|
+
|
|
611
728
|
def report_exception(exception, server_context = {})
|
|
612
729
|
configuration.exception_reporter.call(exception, server_context)
|
|
613
730
|
end
|
|
@@ -628,18 +745,32 @@ module MCP
|
|
|
628
745
|
).to_h
|
|
629
746
|
end
|
|
630
747
|
|
|
748
|
+
# Whether a tool/prompt handler opts in to receiving an `MCP::ServerContext`.
|
|
749
|
+
# Recognizes `:keyrest` (`**kwargs`) because tools are invoked without a positional argument
|
|
750
|
+
# (`tool.call(**args, server_context:)`), soa `**kwargs`-only signature safely captures `server_context:`.
|
|
751
|
+
# Named keyword `server_context` must be `:key` or `:keyreq` - positional parameters (`:req` / `:opt`) that
|
|
752
|
+
# happen to be named `server_context` are excluded because the call site passes `server_context:` as a keyword,
|
|
753
|
+
# and a positional slot would receive the `{server_context: ctx}` Hash instead.
|
|
631
754
|
def accepts_server_context?(method_object)
|
|
632
755
|
parameters = method_object.parameters
|
|
633
756
|
|
|
634
|
-
parameters.any?
|
|
757
|
+
parameters.any? do |type, name|
|
|
758
|
+
type == :keyrest || (name == :server_context && (type == :key || type == :keyreq))
|
|
759
|
+
end
|
|
635
760
|
end
|
|
636
761
|
|
|
637
|
-
def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil, related_request_id: nil)
|
|
762
|
+
def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil, related_request_id: nil, cancellation: nil)
|
|
638
763
|
args = arguments&.transform_keys(&:to_sym) || {}
|
|
639
764
|
|
|
640
765
|
if accepts_server_context?(tool.method(:call))
|
|
641
766
|
progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
|
|
642
|
-
server_context = ServerContext.new(
|
|
767
|
+
server_context = ServerContext.new(
|
|
768
|
+
context,
|
|
769
|
+
progress: progress,
|
|
770
|
+
notification_target: session,
|
|
771
|
+
related_request_id: related_request_id,
|
|
772
|
+
cancellation: cancellation,
|
|
773
|
+
)
|
|
643
774
|
tool.call(**args, server_context: server_context).to_h
|
|
644
775
|
else
|
|
645
776
|
tool.call(**args).to_h
|
data/lib/mcp/server_context.rb
CHANGED
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class ServerContext
|
|
5
|
-
|
|
5
|
+
attr_reader :cancellation
|
|
6
|
+
|
|
7
|
+
def initialize(context, progress:, notification_target:, related_request_id: nil, cancellation: nil)
|
|
6
8
|
@context = context
|
|
7
9
|
@progress = progress
|
|
8
10
|
@notification_target = notification_target
|
|
9
11
|
@related_request_id = related_request_id
|
|
12
|
+
@cancellation = cancellation
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cancelled?
|
|
16
|
+
!!@cancellation&.cancelled?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def raise_if_cancelled!
|
|
20
|
+
@cancellation&.raise_if_cancelled!
|
|
10
21
|
end
|
|
11
22
|
|
|
12
23
|
# Reports progress for the current tool operation.
|
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/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.15.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.15.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
|
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
|