mcp 0.10.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.
@@ -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)
@@ -92,7 +97,7 @@ module JsonRpcHandler
92
97
  end
93
98
 
94
99
  begin
95
- method = method_finder.call(method_name)
100
+ method = method_finder.call(method_name, id)
96
101
 
97
102
  if method.nil?
98
103
  return error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
@@ -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
@@ -117,20 +123,27 @@ module JsonRpcHandler
117
123
  end
118
124
 
119
125
  def handle_request_error(error, id, id_validation_pattern)
120
- error_type = error.respond_to?(:error_type) ? error.error_type : nil
121
-
122
- code, message = case error_type
123
- when :invalid_request then [ErrorCode::INVALID_REQUEST, "Invalid Request"]
124
- when :invalid_params then [ErrorCode::INVALID_PARAMS, "Invalid params"]
125
- when :parse_error then [ErrorCode::PARSE_ERROR, "Parse error"]
126
- when :internal_error then [ErrorCode::INTERNAL_ERROR, "Internal error"]
127
- else [ErrorCode::INTERNAL_ERROR, "Internal error"]
126
+ if error.respond_to?(:error_code) && error.error_code
127
+ code = error.error_code
128
+ message = error.message
129
+ else
130
+ error_type = error.respond_to?(:error_type) ? error.error_type : nil
131
+
132
+ code, message = case error_type
133
+ when :invalid_request then [ErrorCode::INVALID_REQUEST, "Invalid Request"]
134
+ when :invalid_params then [ErrorCode::INVALID_PARAMS, "Invalid params"]
135
+ when :parse_error then [ErrorCode::PARSE_ERROR, "Parse error"]
136
+ when :internal_error then [ErrorCode::INTERNAL_ERROR, "Internal error"]
137
+ else [ErrorCode::INTERNAL_ERROR, "Internal error"]
138
+ end
128
139
  end
129
140
 
141
+ data = error.respond_to?(:error_data) && error.error_data ? error.error_data : error.message
142
+
130
143
  error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
131
144
  code: code,
132
145
  message: message,
133
- data: error.message,
146
+ data: data,
134
147
  })
135
148
  end
136
149
 
@@ -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
@@ -1,24 +1,134 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+ require_relative "../../json_rpc_handler"
5
+ require_relative "../configuration"
6
+ require_relative "../methods"
7
+ require_relative "../version"
8
+
3
9
  module MCP
4
10
  class Client
11
+ # TODO: HTTP GET for SSE streaming is not yet implemented.
12
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server
13
+ # TODO: Resumability and redelivery with Last-Event-ID is not yet implemented.
14
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redelivery
5
15
  class HTTP
6
16
  ACCEPT_HEADER = "application/json, text/event-stream"
17
+ SESSION_ID_HEADER = "Mcp-Session-Id"
18
+ PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version"
7
19
 
8
- attr_reader :url
20
+ attr_reader :url, :session_id, :protocol_version, :server_info
9
21
 
10
- def initialize(url:, headers: {})
22
+ def initialize(url:, headers: {}, &block)
11
23
  @url = url
12
24
  @headers = headers
25
+ @faraday_customizer = block
26
+ @session_id = nil
27
+ @protocol_version = nil
28
+ @server_info = nil
29
+ @connected = false
13
30
  end
14
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
116
+ end
117
+
118
+ # Sends a JSON-RPC request and returns the parsed response body.
119
+ # After a successful `initialize` handshake, the session ID and protocol
120
+ # version returned by the server are captured and automatically included
121
+ # on subsequent requests.
15
122
  def send_request(request:)
16
123
  method = request[:method] || request["method"]
17
124
  params = request[:params] || request["params"]
18
125
 
19
- response = client.post("", request)
20
- validate_response_content_type!(response, method, params)
21
- response.body
126
+ response = client.post("", request, session_headers)
127
+ body = parse_response_body(response, method, params)
128
+
129
+ capture_session_info(method, response, body)
130
+
131
+ body
22
132
  rescue Faraday::BadRequestError => e
23
133
  raise RequestHandlerError.new(
24
134
  "The #{method} request is invalid",
@@ -41,12 +151,25 @@ module MCP
41
151
  original_error: e,
42
152
  )
43
153
  rescue Faraday::ResourceNotFound => e
44
- raise RequestHandlerError.new(
45
- "The #{method} request is not found",
46
- { method: method, params: params },
47
- error_type: :not_found,
48
- original_error: e,
49
- )
154
+ # Per spec, 404 is the session-expired signal only when the request
155
+ # actually carried an `Mcp-Session-Id`. A 404 without a session attached
156
+ # (e.g. wrong URL or a stateless server) surfaces as a generic not-found.
157
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
158
+ if @session_id
159
+ clear_session
160
+ raise SessionExpiredError.new(
161
+ "The #{method} request is not found",
162
+ { method: method, params: params },
163
+ original_error: e,
164
+ )
165
+ else
166
+ raise RequestHandlerError.new(
167
+ "The #{method} request is not found",
168
+ { method: method, params: params },
169
+ error_type: :not_found,
170
+ original_error: e,
171
+ )
172
+ end
50
173
  rescue Faraday::UnprocessableEntityError => e
51
174
  raise RequestHandlerError.new(
52
175
  "The #{method} request is unprocessable",
@@ -63,6 +186,31 @@ module MCP
63
186
  )
64
187
  end
65
188
 
189
+ # Terminates the session by sending an HTTP DELETE to the MCP endpoint
190
+ # with the current `Mcp-Session-Id` header, and clears locally tracked
191
+ # session state afterward. No-op when no session has been established.
192
+ #
193
+ # Per spec, the server MAY respond with HTTP 405 Method Not Allowed when
194
+ # it does not support client-initiated termination, and returns 404 for
195
+ # a session it has already terminated. Both mean the session is gone —
196
+ # the desired end state. Other errors surface to the caller; local
197
+ # session state is cleared either way.
198
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
199
+ def close
200
+ unless @session_id
201
+ clear_session
202
+ return
203
+ end
204
+
205
+ begin
206
+ client.delete("", nil, session_headers)
207
+ rescue Faraday::ClientError => e
208
+ raise unless [404, 405].include?(e.response&.dig(:status))
209
+ ensure
210
+ clear_session
211
+ end
212
+ end
213
+
66
214
  private
67
215
 
68
216
  attr_reader :headers
@@ -78,9 +226,38 @@ module MCP
78
226
  headers.each do |key, value|
79
227
  faraday.headers[key] = value
80
228
  end
229
+
230
+ @faraday_customizer&.call(faraday)
81
231
  end
82
232
  end
83
233
 
234
+ # Per spec, the client MUST include `MCP-Session-Id` (when the server assigned one)
235
+ # and `MCP-Protocol-Version` on all requests after `initialize`.
236
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
237
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header
238
+ def session_headers
239
+ request_headers = {}
240
+ request_headers[SESSION_ID_HEADER] = @session_id if @session_id
241
+ request_headers[PROTOCOL_VERSION_HEADER] = @protocol_version if @protocol_version
242
+ request_headers
243
+ end
244
+
245
+ def capture_session_info(method, response, body)
246
+ return unless method.to_s == Methods::INITIALIZE
247
+
248
+ # Faraday normalizes header names to lowercase.
249
+ session_id = response.headers[SESSION_ID_HEADER.downcase]
250
+ @session_id ||= session_id unless session_id.to_s.empty?
251
+ @protocol_version ||= body.is_a?(Hash) ? body.dig("result", "protocolVersion") : nil
252
+ end
253
+
254
+ def clear_session
255
+ @session_id = nil
256
+ @protocol_version = nil
257
+ @server_info = nil
258
+ @connected = false
259
+ end
260
+
84
261
  def require_faraday!
85
262
  require "faraday"
86
263
  rescue LoadError
@@ -89,14 +266,56 @@ module MCP
89
266
  "See https://rubygems.org/gems/faraday for more details."
90
267
  end
91
268
 
92
- def validate_response_content_type!(response, method, params)
269
+ def require_event_stream_parser!
270
+ require "event_stream_parser"
271
+ rescue LoadError
272
+ raise LoadError, "The 'event_stream_parser' gem is required to parse SSE responses. " \
273
+ "Add it to your Gemfile: gem 'event_stream_parser', '>= 1.0'. " \
274
+ "See https://rubygems.org/gems/event_stream_parser for more details."
275
+ end
276
+
277
+ def parse_response_body(response, method, params)
278
+ # 202 Accepted is the server's ACK for a JSON-RPC notification or response; no body is expected.
279
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
280
+ return if response.status == 202
281
+
93
282
  content_type = response.headers["Content-Type"]
94
- return if content_type&.include?("application/json")
283
+
284
+ if content_type&.include?("text/event-stream")
285
+ parse_sse_response(response.body, method, params)
286
+ elsif content_type&.include?("application/json")
287
+ response.body
288
+ else
289
+ raise RequestHandlerError.new(
290
+ "Unsupported Content-Type: #{content_type.inspect}. Expected application/json or text/event-stream.",
291
+ { method: method, params: params },
292
+ error_type: :unsupported_media_type,
293
+ )
294
+ end
295
+ end
296
+
297
+ def parse_sse_response(body, method, params)
298
+ require_event_stream_parser!
299
+
300
+ json_rpc_response = nil
301
+ parser = EventStreamParser::Parser.new
302
+ parser.feed(body.to_s) do |_type, data, _id|
303
+ next if data.empty?
304
+
305
+ begin
306
+ parsed = JSON.parse(data)
307
+ json_rpc_response = parsed if parsed.is_a?(Hash) && (parsed.key?("result") || parsed.key?("error"))
308
+ rescue JSON::ParserError
309
+ next
310
+ end
311
+ end
312
+
313
+ return json_rpc_response if json_rpc_response
95
314
 
96
315
  raise RequestHandlerError.new(
97
- "Unsupported Content-Type: #{content_type.inspect}. This client only supports JSON responses.",
316
+ "No valid JSON-RPC response found in SSE stream",
98
317
  { method: method, params: params },
99
- error_type: :unsupported_media_type,
318
+ error_type: :parse_error,
100
319
  )
101
320
  end
102
321
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Client
5
+ # Result objects returned by `list_tools`, `list_prompts`, `list_resources`, and `list_resource_templates`.
6
+ # Each carries the page items, an optional opaque `next_cursor` string for continuing pagination,
7
+ # and an optional `meta` hash mirroring the MCP `_meta` response field.
8
+ ListToolsResult = Struct.new(:tools, :next_cursor, :meta, keyword_init: true)
9
+ ListPromptsResult = Struct.new(:prompts, :next_cursor, :meta, keyword_init: true)
10
+ ListResourcesResult = Struct.new(:resources, :next_cursor, :meta, keyword_init: true)
11
+ ListResourceTemplatesResult = Struct.new(:resource_templates, :next_cursor, :meta, keyword_init: true)
12
+ end
13
+ end
@@ -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
- initialize_session unless @initialized
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)