ruby-mcp-client 0.7.0 → 0.7.1

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.
@@ -37,22 +37,24 @@ module MCPClient
37
37
  attr_reader :capabilities
38
38
 
39
39
  # @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)
40
+ # @param options [Hash] Server configuration options
41
+ # @option options [String] :endpoint JSON-RPC endpoint path (default: '/rpc')
42
+ # @option options [Hash] :headers Additional headers to include in requests
43
+ # @option options [Integer] :read_timeout Read timeout in seconds (default: 30)
44
+ # @option options [Integer] :retries Retry attempts on transient errors (default: 3)
45
+ # @option options [Numeric] :retry_backoff Base delay for exponential backoff (default: 1)
46
+ # @option options [String, nil] :name Optional name for this server
47
+ # @option options [Logger, nil] :logger Optional logger
48
+ # @option options [MCPClient::Auth::OAuthProvider, nil] :oauth_provider Optional OAuth provider
49
+ def initialize(base_url:, **options)
50
+ opts = default_options.merge(options)
51
+ super(name: opts[:name])
52
+ @logger = opts[:logger] || Logger.new($stdout, level: Logger::WARN)
51
53
  @logger.progname = self.class.name
52
54
  @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
53
55
 
54
- @max_retries = retries
55
- @retry_backoff = retry_backoff
56
+ @max_retries = opts[:retries]
57
+ @retry_backoff = opts[:retry_backoff]
56
58
 
57
59
  # Validate and normalize base_url
58
60
  raise ArgumentError, "Invalid or insecure server URL: #{base_url}" unless valid_server_url?(base_url)
@@ -73,23 +75,23 @@ module MCPClient
73
75
  end
74
76
 
75
77
  @base_url = build_base_url.call(uri)
76
- @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && endpoint == '/rpc'
78
+ @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && opts[:endpoint] == '/rpc'
77
79
  # If base_url contains a path and we're using default endpoint,
78
80
  # treat the path as the endpoint and use the base URL without path
79
81
  uri.path
80
82
  else
81
83
  # Standard case: base_url is just scheme://host:port, endpoint is separate
82
- endpoint
84
+ opts[:endpoint]
83
85
  end
84
86
 
85
87
  # 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
- })
88
+ @headers = opts[:headers].merge({
89
+ 'Content-Type' => 'application/json',
90
+ 'Accept' => 'application/json',
91
+ 'User-Agent' => "ruby-mcp-client/#{MCPClient::VERSION}"
92
+ })
91
93
 
92
- @read_timeout = read_timeout
94
+ @read_timeout = opts[:read_timeout]
93
95
  @tools = nil
94
96
  @tools_data = nil
95
97
  @request_id = 0
@@ -98,6 +100,7 @@ module MCPClient
98
100
  @initialized = false
99
101
  @http_conn = nil
100
102
  @session_id = nil
103
+ @oauth_provider = opts[:oauth_provider]
101
104
  end
102
105
 
103
106
  # Connect to the MCP server over HTTP
@@ -183,50 +186,34 @@ module MCPClient
183
186
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
184
187
  end
185
188
 
186
- # Override send_http_request to handle session headers for MCP protocol
187
- def send_http_request(request)
188
- conn = http_connection
189
+ # Override apply_request_headers to add session headers for MCP protocol
190
+ def apply_request_headers(req, request)
191
+ super
189
192
 
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
193
+ # Add session header if we have one (for non-initialize requests)
194
+ return unless @session_id && request['method'] != 'initialize'
200
195
 
201
- req.body = request.to_json
202
- end
196
+ req.headers['Mcp-Session-Id'] = @session_id
197
+ @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
198
+ end
203
199
 
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
200
+ # Override handle_successful_response to capture session ID
201
+ def handle_successful_response(response, request)
202
+ super
203
+
204
+ # Capture session ID from initialize response with validation
205
+ return unless request['method'] == 'initialize' && response.success?
220
206
 
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}"
207
+ session_id = response.headers['mcp-session-id'] || response.headers['Mcp-Session-Id']
208
+ if session_id
209
+ if valid_session_id?(session_id)
210
+ @session_id = session_id
211
+ @logger.debug("Captured session ID: #{@session_id}")
212
+ else
213
+ @logger.warn("Invalid session ID format received: #{session_id.inspect}")
214
+ end
215
+ else
216
+ @logger.warn('No session ID found in initialize response headers')
230
217
  end
231
218
  end
232
219
 
@@ -273,6 +260,21 @@ module MCPClient
273
260
 
274
261
  private
275
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
+
276
278
  # Test basic connectivity to the HTTP endpoint
277
279
  # @return [void]
278
280
  # @raise [MCPClient::Errors::ConnectionError] if connection test fails
@@ -37,22 +37,16 @@ module MCPClient
37
37
  attr_reader :capabilities
38
38
 
39
39
  # @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)
40
+ # @param options [Hash] Server configuration options (same as ServerHTTP)
41
+ def initialize(base_url:, **options)
42
+ opts = default_options.merge(options)
43
+ super(name: opts[:name])
44
+ @logger = opts[:logger] || Logger.new($stdout, level: Logger::WARN)
51
45
  @logger.progname = self.class.name
52
46
  @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
53
47
 
54
- @max_retries = retries
55
- @retry_backoff = retry_backoff
48
+ @max_retries = opts[:retries]
49
+ @retry_backoff = opts[:retry_backoff]
56
50
 
57
51
  # Validate and normalize base_url
58
52
  raise ArgumentError, "Invalid or insecure server URL: #{base_url}" unless valid_server_url?(base_url)
@@ -73,25 +67,25 @@ module MCPClient
73
67
  end
74
68
 
75
69
  @base_url = build_base_url.call(uri)
76
- @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && endpoint == '/rpc'
70
+ @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && opts[:endpoint] == '/rpc'
77
71
  # If base_url contains a path and we're using default endpoint,
78
72
  # treat the path as the endpoint and use the base URL without path
79
73
  uri.path
80
74
  else
81
75
  # Standard case: base_url is just scheme://host:port, endpoint is separate
82
- endpoint
76
+ opts[:endpoint]
83
77
  end
84
78
 
85
79
  # 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
80
+ @headers = opts[:headers].merge({
81
+ 'Content-Type' => 'application/json',
82
+ 'Accept' => 'text/event-stream, application/json',
83
+ 'Accept-Encoding' => 'gzip, deflate',
84
+ 'User-Agent' => "ruby-mcp-client/#{MCPClient::VERSION}",
85
+ 'Cache-Control' => 'no-cache'
86
+ })
87
+
88
+ @read_timeout = opts[:read_timeout]
95
89
  @tools = nil
96
90
  @tools_data = nil
97
91
  @request_id = 0
@@ -101,6 +95,7 @@ module MCPClient
101
95
  @http_conn = nil
102
96
  @session_id = nil
103
97
  @last_event_id = nil
98
+ @oauth_provider = opts[:oauth_provider]
104
99
  end
105
100
 
106
101
  # Connect to the MCP server over Streamable HTTP
@@ -196,56 +191,40 @@ module MCPClient
196
191
  end
197
192
  end
198
193
 
199
- # Override send_http_request to handle session headers for MCP protocol
200
- def send_http_request(request)
201
- conn = http_connection
194
+ # Override apply_request_headers to add session and SSE headers for MCP protocol
195
+ def apply_request_headers(req, request)
196
+ super
202
197
 
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
198
+ # Add session header if we have one (for non-initialize requests)
199
+ if @session_id && request['method'] != 'initialize'
200
+ req.headers['Mcp-Session-Id'] = @session_id
201
+ @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
202
+ end
213
203
 
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
204
+ # Add Last-Event-ID header for resumability (if available)
205
+ return unless @last_event_id
219
206
 
220
- req.body = request.to_json
221
- end
207
+ req.headers['Last-Event-ID'] = @last_event_id
208
+ @logger.debug("Adding Last-Event-ID header: #{@last_event_id}")
209
+ end
222
210
 
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
211
+ # Override handle_successful_response to capture session ID
212
+ def handle_successful_response(response, request)
213
+ super
214
+
215
+ # Capture session ID from initialize response with validation
216
+ return unless request['method'] == 'initialize' && response.success?
239
217
 
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}"
218
+ session_id = response.headers['mcp-session-id'] || response.headers['Mcp-Session-Id']
219
+ if session_id
220
+ if valid_session_id?(session_id)
221
+ @session_id = session_id
222
+ @logger.debug("Captured session ID: #{@session_id}")
223
+ else
224
+ @logger.warn("Invalid session ID format received: #{session_id.inspect}")
225
+ end
226
+ else
227
+ @logger.warn('No session ID found in initialize response headers')
249
228
  end
250
229
  end
251
230
 
@@ -282,6 +261,21 @@ module MCPClient
282
261
 
283
262
  private
284
263
 
264
+ # Default options for server initialization
265
+ # @return [Hash] Default options
266
+ def default_options
267
+ {
268
+ endpoint: '/rpc',
269
+ headers: {},
270
+ read_timeout: DEFAULT_READ_TIMEOUT,
271
+ retries: DEFAULT_MAX_RETRIES,
272
+ retry_backoff: 1,
273
+ name: nil,
274
+ logger: nil,
275
+ oauth_provider: nil
276
+ }
277
+ end
278
+
285
279
  # Test basic connectivity to the HTTP endpoint
286
280
  # @return [void]
287
281
  # @raise [MCPClient::Errors::ConnectionError] if connection test fails
@@ -2,7 +2,7 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.7.0'
5
+ VERSION = '0.7.1'
6
6
 
7
7
  # JSON-RPC handshake protocol version (date-based)
8
8
  PROTOCOL_VERSION = '2024-11-05'
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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mcp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Szymon Kurcab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-18 00:00:00.000000000 Z
11
+ date: 2025-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -105,11 +105,14 @@ files:
105
105
  - LICENSE
106
106
  - README.md
107
107
  - lib/mcp_client.rb
108
+ - lib/mcp_client/auth.rb
109
+ - lib/mcp_client/auth/oauth_provider.rb
108
110
  - lib/mcp_client/client.rb
109
111
  - lib/mcp_client/config_parser.rb
110
112
  - lib/mcp_client/errors.rb
111
113
  - lib/mcp_client/http_transport_base.rb
112
114
  - lib/mcp_client/json_rpc_common.rb
115
+ - lib/mcp_client/oauth_client.rb
113
116
  - lib/mcp_client/server_base.rb
114
117
  - lib/mcp_client/server_factory.rb
115
118
  - lib/mcp_client/server_http.rb