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.
- checksums.yaml +4 -4
- data/README.md +71 -0
- data/lib/mcp_client/auth/oauth_provider.rb +514 -0
- data/lib/mcp_client/auth.rb +315 -0
- data/lib/mcp_client/http_transport_base.rb +41 -4
- data/lib/mcp_client/oauth_client.rb +127 -0
- data/lib/mcp_client/server_http.rb +63 -61
- data/lib/mcp_client/server_streamable_http.rb +63 -69
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +2 -0
- metadata +5 -2
@@ -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
|
41
|
-
# @
|
42
|
-
# @
|
43
|
-
# @
|
44
|
-
# @
|
45
|
-
# @
|
46
|
-
# @
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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
|
187
|
-
def
|
188
|
-
|
189
|
+
# Override apply_request_headers to add session headers for MCP protocol
|
190
|
+
def apply_request_headers(req, request)
|
191
|
+
super
|
189
192
|
|
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
|
193
|
+
# Add session header if we have one (for non-initialize requests)
|
194
|
+
return unless @session_id && request['method'] != 'initialize'
|
200
195
|
|
201
|
-
|
202
|
-
|
196
|
+
req.headers['Mcp-Session-Id'] = @session_id
|
197
|
+
@logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
|
198
|
+
end
|
203
199
|
|
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
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
200
|
-
def
|
201
|
-
|
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
|
-
|
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
|
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
|
-
|
215
|
-
|
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
|
-
|
221
|
-
|
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
|
-
|
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
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
data/lib/mcp_client/version.rb
CHANGED
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.
|
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-
|
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
|