ruby-mcp-client 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ require 'monitor'
6
6
  require 'logger'
7
7
  require 'faraday'
8
8
  require 'faraday/retry'
9
+ require 'faraday/follow_redirects'
9
10
 
10
11
  module MCPClient
11
12
  # Implementation of MCP server that communicates via HTTP requests/responses
@@ -37,22 +38,22 @@ module MCPClient
37
38
  attr_reader :capabilities
38
39
 
39
40
  # @param base_url [String] The base URL of the MCP server
40
- # @param endpoint [String] The JSON-RPC endpoint path (default: '/rpc')
41
- # @param headers [Hash] Additional headers to include in requests
42
- # @param read_timeout [Integer] Read timeout in seconds (default: 30)
43
- # @param retries [Integer] number of retry attempts on transient errors (default: 3)
44
- # @param retry_backoff [Numeric] base delay in seconds for exponential backoff (default: 1)
45
- # @param name [String, nil] optional name for this server
46
- # @param logger [Logger, nil] optional logger
47
- def initialize(base_url:, endpoint: '/rpc', headers: {}, read_timeout: DEFAULT_READ_TIMEOUT,
48
- retries: DEFAULT_MAX_RETRIES, retry_backoff: 1, name: nil, logger: nil)
49
- super(name: name)
50
- @logger = logger || Logger.new($stdout, level: Logger::WARN)
51
- @logger.progname = self.class.name
52
- @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
53
-
54
- @max_retries = retries
55
- @retry_backoff = retry_backoff
41
+ # @param options [Hash] Server configuration options
42
+ # @option options [String] :endpoint JSON-RPC endpoint path (default: '/rpc')
43
+ # @option options [Hash] :headers Additional headers to include in requests
44
+ # @option options [Integer] :read_timeout Read timeout in seconds (default: 30)
45
+ # @option options [Integer] :retries Retry attempts on transient errors (default: 3)
46
+ # @option options [Numeric] :retry_backoff Base delay for exponential backoff (default: 1)
47
+ # @option options [String, nil] :name Optional name for this server
48
+ # @option options [Logger, nil] :logger Optional logger
49
+ # @option options [MCPClient::Auth::OAuthProvider, nil] :oauth_provider Optional OAuth provider
50
+ def initialize(base_url:, **options)
51
+ opts = default_options.merge(options)
52
+ super(name: opts[:name])
53
+ initialize_logger(opts[:logger])
54
+
55
+ @max_retries = opts[:retries]
56
+ @retry_backoff = opts[:retry_backoff]
56
57
 
57
58
  # Validate and normalize base_url
58
59
  raise ArgumentError, "Invalid or insecure server URL: #{base_url}" unless valid_server_url?(base_url)
@@ -73,23 +74,23 @@ module MCPClient
73
74
  end
74
75
 
75
76
  @base_url = build_base_url.call(uri)
76
- @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && endpoint == '/rpc'
77
+ @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && opts[:endpoint] == '/rpc'
77
78
  # If base_url contains a path and we're using default endpoint,
78
79
  # treat the path as the endpoint and use the base URL without path
79
80
  uri.path
80
81
  else
81
82
  # Standard case: base_url is just scheme://host:port, endpoint is separate
82
- endpoint
83
+ opts[:endpoint]
83
84
  end
84
85
 
85
86
  # Set up headers for HTTP requests
86
- @headers = headers.merge({
87
- 'Content-Type' => 'application/json',
88
- 'Accept' => 'application/json',
89
- 'User-Agent' => "ruby-mcp-client/#{MCPClient::VERSION}"
90
- })
87
+ @headers = opts[:headers].merge({
88
+ 'Content-Type' => 'application/json',
89
+ 'Accept' => 'application/json',
90
+ 'User-Agent' => "ruby-mcp-client/#{MCPClient::VERSION}"
91
+ })
91
92
 
92
- @read_timeout = read_timeout
93
+ @read_timeout = opts[:read_timeout]
93
94
  @tools = nil
94
95
  @tools_data = nil
95
96
  @request_id = 0
@@ -98,6 +99,7 @@ module MCPClient
98
99
  @initialized = false
99
100
  @http_conn = nil
100
101
  @session_id = nil
102
+ @oauth_provider = opts[:oauth_provider]
101
103
  end
102
104
 
103
105
  # Connect to the MCP server over HTTP
@@ -183,50 +185,34 @@ module MCPClient
183
185
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
184
186
  end
185
187
 
186
- # Override send_http_request to handle session headers for MCP protocol
187
- def send_http_request(request)
188
- conn = http_connection
188
+ # Override apply_request_headers to add session headers for MCP protocol
189
+ def apply_request_headers(req, request)
190
+ super
189
191
 
190
- begin
191
- response = conn.post(@endpoint) do |req|
192
- # Apply all headers including custom ones
193
- @headers.each { |k, v| req.headers[k] = v }
194
-
195
- # Add session header if we have one (for non-initialize requests)
196
- if @session_id && request['method'] != 'initialize'
197
- req.headers['Mcp-Session-Id'] = @session_id
198
- @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
199
- end
192
+ # Add session header if we have one (for non-initialize requests)
193
+ return unless @session_id && request['method'] != 'initialize'
200
194
 
201
- req.body = request.to_json
202
- end
195
+ req.headers['Mcp-Session-Id'] = @session_id
196
+ @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
197
+ end
203
198
 
204
- handle_http_error_response(response) unless response.success?
205
-
206
- # Capture session ID from initialize response with validation
207
- if request['method'] == 'initialize' && response.success?
208
- session_id = response.headers['mcp-session-id'] || response.headers['Mcp-Session-Id']
209
- if session_id
210
- if valid_session_id?(session_id)
211
- @session_id = session_id
212
- @logger.debug("Captured session ID: #{@session_id}")
213
- else
214
- @logger.warn("Invalid session ID format received: #{session_id.inspect}")
215
- end
216
- else
217
- @logger.warn('No session ID found in initialize response headers')
218
- end
219
- end
199
+ # Override handle_successful_response to capture session ID
200
+ def handle_successful_response(response, request)
201
+ super
202
+
203
+ # Capture session ID from initialize response with validation
204
+ return unless request['method'] == 'initialize' && response.success?
220
205
 
221
- log_response(response)
222
- response
223
- rescue Faraday::UnauthorizedError, Faraday::ForbiddenError => e
224
- error_status = e.response ? e.response[:status] : 'unknown'
225
- raise MCPClient::Errors::ConnectionError, "Authorization failed: HTTP #{error_status}"
226
- rescue Faraday::ConnectionFailed => e
227
- raise MCPClient::Errors::ConnectionError, "Server connection lost: #{e.message}"
228
- rescue Faraday::Error => e
229
- raise MCPClient::Errors::TransportError, "HTTP request failed: #{e.message}"
206
+ session_id = response.headers['mcp-session-id'] || response.headers['Mcp-Session-Id']
207
+ if session_id
208
+ if valid_session_id?(session_id)
209
+ @session_id = session_id
210
+ @logger.debug("Captured session ID: #{@session_id}")
211
+ else
212
+ @logger.warn("Invalid session ID format received: #{session_id.inspect}")
213
+ end
214
+ else
215
+ @logger.warn('No session ID found in initialize response headers')
230
216
  end
231
217
  end
232
218
 
@@ -273,6 +259,21 @@ module MCPClient
273
259
 
274
260
  private
275
261
 
262
+ # Default options for server initialization
263
+ # @return [Hash] Default options
264
+ def default_options
265
+ {
266
+ endpoint: '/rpc',
267
+ headers: {},
268
+ read_timeout: DEFAULT_READ_TIMEOUT,
269
+ retries: DEFAULT_MAX_RETRIES,
270
+ retry_backoff: 1,
271
+ name: nil,
272
+ logger: nil,
273
+ oauth_provider: nil
274
+ }
275
+ end
276
+
276
277
  # Test basic connectivity to the HTTP endpoint
277
278
  # @return [void]
278
279
  # @raise [MCPClient::Errors::ConnectionError] if connection test fails
@@ -70,6 +70,13 @@ module MCPClient
70
70
 
71
71
  @server_info = result['serverInfo']
72
72
  @capabilities = result['capabilities']
73
+
74
+ # Send initialized notification to acknowledge completion of initialization
75
+ initialized_notification = build_jsonrpc_notification('notifications/initialized', {})
76
+ post_json_rpc_request(initialized_notification)
77
+
78
+ # Small delay to ensure server processes the notification
79
+ sleep(0.1)
73
80
  end
74
81
 
75
82
  # Send a JSON-RPC request to the server and wait for result
@@ -133,6 +140,7 @@ module MCPClient
133
140
  def create_json_rpc_connection(base_url)
134
141
  Faraday.new(url: base_url) do |f|
135
142
  f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
143
+ f.response :follow_redirects, limit: 3
136
144
  f.options.open_timeout = @read_timeout
137
145
  f.options.timeout = @read_timeout
138
146
  f.adapter Faraday.default_adapter
@@ -191,6 +191,7 @@ module MCPClient
191
191
  f.options.open_timeout = 10
192
192
  f.options.timeout = nil
193
193
  f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
194
+ f.response :follow_redirects, limit: 3
194
195
  f.adapter Faraday.default_adapter
195
196
  end
196
197
 
@@ -31,9 +31,9 @@ module MCPClient
31
31
  data = JSON.parse(event[:data])
32
32
 
33
33
  return if process_error_in_message(data)
34
- return if process_notification(data)
34
+ return if process_notification?(data)
35
35
 
36
- process_response(data)
36
+ process_response?(data)
37
37
  rescue MCPClient::Errors::ConnectionError
38
38
  raise
39
39
  rescue JSON::ParserError => e
@@ -61,7 +61,7 @@ module MCPClient
61
61
  # Process a JSON-RPC notification (no id => notification)
62
62
  # @param data [Hash] the parsed JSON payload
63
63
  # @return [Boolean] true if we saw & handled a notification
64
- def process_notification(data)
64
+ def process_notification?(data)
65
65
  return false unless data['method'] && !data.key?('id')
66
66
 
67
67
  @notification_callback&.call(data['method'], data['params'])
@@ -71,7 +71,7 @@ module MCPClient
71
71
  # Process a JSON-RPC response (id => response)
72
72
  # @param data [Hash] the parsed JSON payload
73
73
  # @return [Boolean] true if we saw & handled a response
74
- def process_response(data)
74
+ def process_response?(data)
75
75
  return false unless data['id']
76
76
 
77
77
  @mutex.synchronize do
@@ -6,6 +6,7 @@ require 'monitor'
6
6
  require 'logger'
7
7
  require 'faraday'
8
8
  require 'faraday/retry'
9
+ require 'faraday/follow_redirects'
9
10
 
10
11
  module MCPClient
11
12
  # Implementation of MCP server that communicates via Server-Sent Events (SSE)
@@ -56,13 +57,11 @@ module MCPClient
56
57
  def initialize(base_url:, headers: {}, read_timeout: 30, ping: 10,
57
58
  retries: 0, retry_backoff: 1, name: nil, logger: nil)
58
59
  super(name: name)
59
- @logger = logger || Logger.new($stdout, level: Logger::WARN)
60
- @logger.progname = self.class.name
61
- @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
60
+ initialize_logger(logger)
62
61
  @max_retries = retries
63
62
  @retry_backoff = retry_backoff
64
- # Normalize base_url: strip any trailing slash, use exactly as provided
65
- @base_url = base_url.chomp('/')
63
+ # Normalize base_url: preserve trailing slash if explicitly provided for SSE endpoints
64
+ @base_url = base_url
66
65
  @headers = headers.merge({
67
66
  'Accept' => 'text/event-stream',
68
67
  'Cache-Control' => 'no-cache',
@@ -369,53 +368,86 @@ module MCPClient
369
368
  record_activity if chunk.include?('event:')
370
369
 
371
370
  # Check for direct JSON error responses (which aren't proper SSE events)
372
- if chunk.start_with?('{') && chunk.include?('"error"') &&
373
- (chunk.include?('Unauthorized') || chunk.include?('authentication'))
374
- begin
375
- data = JSON.parse(chunk)
376
- if data['error']
377
- error_message = data['error']['message'] || 'Unknown server error'
371
+ handle_json_error_response(chunk)
372
+
373
+ event_buffers = extract_complete_events(chunk)
374
+
375
+ # Process extracted events outside the mutex to avoid deadlocks
376
+ event_buffers&.each { |event_data| parse_and_handle_sse_event(event_data) }
377
+ end
378
+
379
+ # Check if the error represents an authorization error
380
+ # @param error_message [String] The error message from the server
381
+ # @param error_code [Integer, nil] The error code if available
382
+ # @return [Boolean] True if it's an authorization error
383
+ # @private
384
+ def authorization_error?(error_message, error_code)
385
+ return true if error_message.include?('Unauthorized') || error_message.include?('authentication')
386
+ return true if [401, -32_000].include?(error_code)
378
387
 
379
- @mutex.synchronize do
380
- @auth_error = "Authorization failed: #{error_message}"
388
+ false
389
+ end
381
390
 
382
- @connection_established = false
383
- @connection_cv.broadcast
384
- end
391
+ # Handle JSON error responses embedded in SSE chunks
392
+ # @param chunk [String] the chunk to check for JSON errors
393
+ # @return [void]
394
+ # @raise [MCPClient::Errors::ConnectionError] if authentication error is found
395
+ # @private
396
+ def handle_json_error_response(chunk)
397
+ return unless chunk.start_with?('{') && chunk.include?('"error"') &&
398
+ (chunk.include?('Unauthorized') || chunk.include?('authentication'))
385
399
 
386
- raise MCPClient::Errors::ConnectionError, "Authorization failed: #{error_message}"
400
+ begin
401
+ data = JSON.parse(chunk)
402
+ if data['error']
403
+ error_message = data['error']['message'] || 'Unknown server error'
404
+
405
+ @mutex.synchronize do
406
+ @auth_error = "Authorization failed: #{error_message}"
407
+ @connection_established = false
408
+ @connection_cv.broadcast
387
409
  end
388
- rescue JSON::ParserError
389
- # Not valid JSON, process normally
410
+
411
+ raise MCPClient::Errors::ConnectionError, "Authorization failed: #{error_message}"
390
412
  end
413
+ rescue JSON::ParserError
414
+ # Not valid JSON, process normally
391
415
  end
416
+ end
392
417
 
418
+ # Extract complete SSE events from the buffer
419
+ # @param chunk [String] the chunk to add to the buffer
420
+ # @return [Array<String>, nil] array of complete events or nil if none
421
+ # @private
422
+ def extract_complete_events(chunk)
393
423
  event_buffers = nil
394
424
  @mutex.synchronize do
395
425
  @buffer += chunk
396
426
 
397
427
  # Extract all complete events from the buffer
428
+ # Handle both Unix (\n\n) and Windows (\r\n\r\n) line endings
398
429
  event_buffers = []
399
- while (event_end = @buffer.index("\n\n"))
400
- event_data = @buffer.slice!(0, event_end + 2)
430
+ while (event_end = @buffer.index("\n\n") || @buffer.index("\r\n\r\n"))
431
+ event_data = extract_single_event(event_end)
401
432
  event_buffers << event_data
402
433
  end
403
434
  end
404
-
405
- # Process extracted events outside the mutex to avoid deadlocks
406
- event_buffers&.each { |event_data| parse_and_handle_sse_event(event_data) }
435
+ event_buffers
407
436
  end
408
437
 
409
- # Check if the error represents an authorization error
410
- # @param error_message [String] The error message from the server
411
- # @param error_code [Integer, nil] The error code if available
412
- # @return [Boolean] True if it's an authorization error
438
+ # Extract a single event from the buffer
439
+ # @param event_end [Integer] the position where the event ends
440
+ # @return [String] the extracted event data
413
441
  # @private
414
- def authorization_error?(error_message, error_code)
415
- return true if error_message.include?('Unauthorized') || error_message.include?('authentication')
416
- return true if [401, -32_000].include?(error_code)
417
-
418
- false
442
+ def extract_single_event(event_end)
443
+ # Determine the line ending style and extract accordingly
444
+ crlf_index = @buffer.index("\r\n\r\n")
445
+ lf_index = @buffer.index("\n\n")
446
+ if crlf_index && (lf_index.nil? || crlf_index < lf_index)
447
+ @buffer.slice!(0, event_end + 4) # \r\n\r\n is 4 chars
448
+ else
449
+ @buffer.slice!(0, event_end + 2) # \n\n is 2 chars
450
+ end
419
451
  end
420
452
 
421
453
  # Handle authorization error in SSE message
@@ -39,9 +39,7 @@ module MCPClient
39
39
  @next_id = 1
40
40
  @pending = {}
41
41
  @initialized = false
42
- @logger = logger || Logger.new($stdout, level: Logger::WARN)
43
- @logger.progname = self.class.name
44
- @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
42
+ initialize_logger(logger)
45
43
  @max_retries = retries
46
44
  @retry_backoff = retry_backoff
47
45
  @read_timeout = read_timeout
@@ -6,6 +6,7 @@ require 'monitor'
6
6
  require 'logger'
7
7
  require 'faraday'
8
8
  require 'faraday/retry'
9
+ require 'faraday/follow_redirects'
9
10
 
10
11
  module MCPClient
11
12
  # Implementation of MCP server that communicates via Streamable HTTP transport
@@ -37,22 +38,14 @@ module MCPClient
37
38
  attr_reader :capabilities
38
39
 
39
40
  # @param base_url [String] The base URL of the MCP server
40
- # @param endpoint [String] The JSON-RPC endpoint path (default: '/rpc')
41
- # @param headers [Hash] Additional headers to include in requests
42
- # @param read_timeout [Integer] Read timeout in seconds (default: 30)
43
- # @param retries [Integer] number of retry attempts on transient errors (default: 3)
44
- # @param retry_backoff [Numeric] base delay in seconds for exponential backoff (default: 1)
45
- # @param name [String, nil] optional name for this server
46
- # @param logger [Logger, nil] optional logger
47
- def initialize(base_url:, endpoint: '/rpc', headers: {}, read_timeout: DEFAULT_READ_TIMEOUT,
48
- retries: DEFAULT_MAX_RETRIES, retry_backoff: 1, name: nil, logger: nil)
49
- super(name: name)
50
- @logger = logger || Logger.new($stdout, level: Logger::WARN)
51
- @logger.progname = self.class.name
52
- @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
53
-
54
- @max_retries = retries
55
- @retry_backoff = retry_backoff
41
+ # @param options [Hash] Server configuration options (same as ServerHTTP)
42
+ def initialize(base_url:, **options)
43
+ opts = default_options.merge(options)
44
+ super(name: opts[:name])
45
+ initialize_logger(opts[:logger])
46
+
47
+ @max_retries = opts[:retries]
48
+ @retry_backoff = opts[:retry_backoff]
56
49
 
57
50
  # Validate and normalize base_url
58
51
  raise ArgumentError, "Invalid or insecure server URL: #{base_url}" unless valid_server_url?(base_url)
@@ -73,25 +66,25 @@ module MCPClient
73
66
  end
74
67
 
75
68
  @base_url = build_base_url.call(uri)
76
- @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && endpoint == '/rpc'
69
+ @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && opts[:endpoint] == '/rpc'
77
70
  # If base_url contains a path and we're using default endpoint,
78
71
  # treat the path as the endpoint and use the base URL without path
79
72
  uri.path
80
73
  else
81
74
  # Standard case: base_url is just scheme://host:port, endpoint is separate
82
- endpoint
75
+ opts[:endpoint]
83
76
  end
84
77
 
85
78
  # Set up headers for Streamable HTTP requests
86
- @headers = headers.merge({
87
- 'Content-Type' => 'application/json',
88
- 'Accept' => 'text/event-stream, application/json',
89
- 'Accept-Encoding' => 'gzip, deflate',
90
- 'User-Agent' => "ruby-mcp-client/#{MCPClient::VERSION}",
91
- 'Cache-Control' => 'no-cache'
92
- })
93
-
94
- @read_timeout = read_timeout
79
+ @headers = opts[:headers].merge({
80
+ 'Content-Type' => 'application/json',
81
+ 'Accept' => 'text/event-stream, application/json',
82
+ 'Accept-Encoding' => 'gzip, deflate',
83
+ 'User-Agent' => "ruby-mcp-client/#{MCPClient::VERSION}",
84
+ 'Cache-Control' => 'no-cache'
85
+ })
86
+
87
+ @read_timeout = opts[:read_timeout]
95
88
  @tools = nil
96
89
  @tools_data = nil
97
90
  @request_id = 0
@@ -101,6 +94,7 @@ module MCPClient
101
94
  @http_conn = nil
102
95
  @session_id = nil
103
96
  @last_event_id = nil
97
+ @oauth_provider = opts[:oauth_provider]
104
98
  end
105
99
 
106
100
  # Connect to the MCP server over Streamable HTTP
@@ -196,56 +190,40 @@ module MCPClient
196
190
  end
197
191
  end
198
192
 
199
- # Override send_http_request to handle session headers for MCP protocol
200
- def send_http_request(request)
201
- conn = http_connection
193
+ # Override apply_request_headers to add session and SSE headers for MCP protocol
194
+ def apply_request_headers(req, request)
195
+ super
202
196
 
203
- begin
204
- response = conn.post(@endpoint) do |req|
205
- # Apply all headers including custom ones
206
- @headers.each { |k, v| req.headers[k] = v }
207
-
208
- # Add session header if we have one (for non-initialize requests)
209
- if @session_id && request['method'] != 'initialize'
210
- req.headers['Mcp-Session-Id'] = @session_id
211
- @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
212
- end
197
+ # Add session header if we have one (for non-initialize requests)
198
+ if @session_id && request['method'] != 'initialize'
199
+ req.headers['Mcp-Session-Id'] = @session_id
200
+ @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
201
+ end
213
202
 
214
- # Add Last-Event-ID header for resumability (if available)
215
- if @last_event_id
216
- req.headers['Last-Event-ID'] = @last_event_id
217
- @logger.debug("Adding Last-Event-ID header: #{@last_event_id}")
218
- end
203
+ # Add Last-Event-ID header for resumability (if available)
204
+ return unless @last_event_id
219
205
 
220
- req.body = request.to_json
221
- end
206
+ req.headers['Last-Event-ID'] = @last_event_id
207
+ @logger.debug("Adding Last-Event-ID header: #{@last_event_id}")
208
+ end
222
209
 
223
- handle_http_error_response(response) unless response.success?
224
-
225
- # Capture session ID from initialize response with validation
226
- if request['method'] == 'initialize' && response.success?
227
- session_id = response.headers['mcp-session-id'] || response.headers['Mcp-Session-Id']
228
- if session_id
229
- if valid_session_id?(session_id)
230
- @session_id = session_id
231
- @logger.debug("Captured session ID: #{@session_id}")
232
- else
233
- @logger.warn("Invalid session ID format received: #{session_id.inspect}")
234
- end
235
- else
236
- @logger.warn('No session ID found in initialize response headers')
237
- end
238
- end
210
+ # Override handle_successful_response to capture session ID
211
+ def handle_successful_response(response, request)
212
+ super
213
+
214
+ # Capture session ID from initialize response with validation
215
+ return unless request['method'] == 'initialize' && response.success?
239
216
 
240
- log_response(response)
241
- response
242
- rescue Faraday::UnauthorizedError, Faraday::ForbiddenError => e
243
- error_status = e.response ? e.response[:status] : 'unknown'
244
- raise MCPClient::Errors::ConnectionError, "Authorization failed: HTTP #{error_status}"
245
- rescue Faraday::ConnectionFailed => e
246
- raise MCPClient::Errors::ConnectionError, "Server connection lost: #{e.message}"
247
- rescue Faraday::Error => e
248
- raise MCPClient::Errors::TransportError, "HTTP request failed: #{e.message}"
217
+ session_id = response.headers['mcp-session-id'] || response.headers['Mcp-Session-Id']
218
+ if session_id
219
+ if valid_session_id?(session_id)
220
+ @session_id = session_id
221
+ @logger.debug("Captured session ID: #{@session_id}")
222
+ else
223
+ @logger.warn("Invalid session ID format received: #{session_id.inspect}")
224
+ end
225
+ else
226
+ @logger.warn('No session ID found in initialize response headers')
249
227
  end
250
228
  end
251
229
 
@@ -282,6 +260,21 @@ module MCPClient
282
260
 
283
261
  private
284
262
 
263
+ # Default options for server initialization
264
+ # @return [Hash] Default options
265
+ def default_options
266
+ {
267
+ endpoint: '/rpc',
268
+ headers: {},
269
+ read_timeout: DEFAULT_READ_TIMEOUT,
270
+ retries: DEFAULT_MAX_RETRIES,
271
+ retry_backoff: 1,
272
+ name: nil,
273
+ logger: nil,
274
+ oauth_provider: nil
275
+ }
276
+ end
277
+
285
278
  # Test basic connectivity to the HTTP endpoint
286
279
  # @return [void]
287
280
  # @raise [MCPClient::Errors::ConnectionError] if connection test fails
@@ -2,11 +2,8 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.7.0'
5
+ VERSION = '0.7.2'
6
6
 
7
- # JSON-RPC handshake protocol version (date-based)
8
- PROTOCOL_VERSION = '2024-11-05'
9
-
10
- # Protocol version for HTTP and Streamable HTTP transports
11
- HTTP_PROTOCOL_VERSION = '2025-03-26'
7
+ # MCP protocol version (date-based) - unified across all transports
8
+ PROTOCOL_VERSION = '2025-03-26'
12
9
  end
data/lib/mcp_client.rb CHANGED
@@ -12,6 +12,8 @@ require_relative 'mcp_client/server_factory'
12
12
  require_relative 'mcp_client/client'
13
13
  require_relative 'mcp_client/version'
14
14
  require_relative 'mcp_client/config_parser'
15
+ require_relative 'mcp_client/auth'
16
+ require_relative 'mcp_client/oauth_client'
15
17
 
16
18
  # Model Context Protocol (MCP) Client module
17
19
  # Provides a standardized way for agents to communicate with external tools and services