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.
- checksums.yaml +4 -4
- data/README.md +119 -0
- data/lib/mcp_client/auth/oauth_provider.rb +514 -0
- data/lib/mcp_client/auth.rb +315 -0
- data/lib/mcp_client/config_parser.rb +8 -8
- data/lib/mcp_client/http_transport_base.rb +42 -5
- data/lib/mcp_client/oauth_client.rb +127 -0
- data/lib/mcp_client/server_base.rb +18 -0
- data/lib/mcp_client/server_http.rb +65 -64
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +8 -0
- data/lib/mcp_client/server_sse/reconnect_monitor.rb +1 -0
- data/lib/mcp_client/server_sse/sse_parser.rb +4 -4
- data/lib/mcp_client/server_sse.rb +65 -33
- data/lib/mcp_client/server_stdio.rb +1 -3
- data/lib/mcp_client/server_streamable_http.rb +65 -72
- data/lib/mcp_client/version.rb +3 -6
- data/lib/mcp_client.rb +2 -0
- metadata +19 -2
@@ -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
|
41
|
-
# @
|
42
|
-
# @
|
43
|
-
# @
|
44
|
-
# @
|
45
|
-
# @
|
46
|
-
# @
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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
|
187
|
-
def
|
188
|
-
|
188
|
+
# Override apply_request_headers to add session headers for MCP protocol
|
189
|
+
def apply_request_headers(req, request)
|
190
|
+
super
|
189
191
|
|
190
|
-
|
191
|
-
|
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
|
-
|
202
|
-
|
195
|
+
req.headers['Mcp-Session-Id'] = @session_id
|
196
|
+
@logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
|
197
|
+
end
|
203
198
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
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:
|
65
|
-
@base_url = base_url
|
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
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
-
|
380
|
-
|
388
|
+
false
|
389
|
+
end
|
381
390
|
|
382
|
-
|
383
|
-
|
384
|
-
|
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
|
-
|
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
|
-
|
389
|
-
|
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 =
|
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
|
-
#
|
410
|
-
# @param
|
411
|
-
# @
|
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
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
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
|
-
|
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
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
200
|
-
def
|
201
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
215
|
-
|
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
|
-
|
221
|
-
|
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
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
data/lib/mcp_client/version.rb
CHANGED
@@ -2,11 +2,8 @@
|
|
2
2
|
|
3
3
|
module MCPClient
|
4
4
|
# Current version of the MCP client gem
|
5
|
-
VERSION = '0.7.
|
5
|
+
VERSION = '0.7.2'
|
6
6
|
|
7
|
-
#
|
8
|
-
PROTOCOL_VERSION = '
|
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
|