ruby-mcp-client 0.6.2 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'mcp_client/json_rpc_common'
3
+ require_relative '../json_rpc_common'
4
4
 
5
5
  module MCPClient
6
6
  class ServerSSE
@@ -68,8 +68,8 @@ module MCPClient
68
68
  result = send_jsonrpc_request(json_rpc_request)
69
69
  return unless result.is_a?(Hash)
70
70
 
71
- @server_info = result['serverInfo'] if result.key?('serverInfo')
72
- @capabilities = result['capabilities'] if result.key?('capabilities')
71
+ @server_info = result['serverInfo']
72
+ @capabilities = result['capabilities']
73
73
  end
74
74
 
75
75
  # Send a JSON-RPC request to the server and wait for result
@@ -97,7 +97,7 @@ module MCPClient
97
97
  rescue Errno::ECONNREFUSED => e
98
98
  raise MCPClient::Errors::ConnectionError, "Server connection lost: #{e.message}"
99
99
  rescue StandardError => e
100
- method_name = request[:method] || request['method']
100
+ method_name = request['method']
101
101
  raise MCPClient::Errors::ToolCallError, "Error executing request '#{method_name}': #{e.message}"
102
102
  end
103
103
  end
@@ -166,7 +166,7 @@ module MCPClient
166
166
  # @return [Hash] the result data
167
167
  # @raise [MCPClient::Errors::ConnectionError, MCPClient::Errors::ToolCallError] on errors
168
168
  def wait_for_sse_result(request)
169
- request_id = request[:id]
169
+ request_id = request['id']
170
170
  start_time = Time.now
171
171
  timeout = @read_timeout || 10
172
172
 
@@ -11,12 +11,12 @@ module MCPClient
11
11
  # Implementation of MCP server that communicates via Server-Sent Events (SSE)
12
12
  # Useful for communicating with remote MCP servers over HTTP
13
13
  class ServerSSE < ServerBase
14
- require 'mcp_client/server_sse/sse_parser'
15
- require 'mcp_client/server_sse/json_rpc_transport'
14
+ require_relative 'server_sse/sse_parser'
15
+ require_relative 'server_sse/json_rpc_transport'
16
16
 
17
17
  include SseParser
18
18
  include JsonRpcTransport
19
- require 'mcp_client/server_sse/reconnect_monitor'
19
+ require_relative 'server_sse/reconnect_monitor'
20
20
 
21
21
  include ReconnectMonitor
22
22
  # Ratio of close_after timeout to ping interval
@@ -35,11 +35,15 @@ module MCPClient
35
35
  # @return [String] The base URL of the MCP server
36
36
  # @!attribute [r] tools
37
37
  # @return [Array<MCPClient::Tool>, nil] List of available tools (nil if not fetched yet)
38
- # @!attribute [r] server_info
39
- # @return [Hash, nil] Server information from initialize response
40
- # @!attribute [r] capabilities
41
- # @return [Hash, nil] Server capabilities from initialize response
42
- attr_reader :base_url, :tools, :server_info, :capabilities
38
+ attr_reader :base_url, :tools
39
+
40
+ # Server information from initialize response
41
+ # @return [Hash, nil] Server information
42
+ attr_reader :server_info
43
+
44
+ # Server capabilities from initialize response
45
+ # @return [Hash, nil] Server capabilities
46
+ attr_reader :capabilities
43
47
 
44
48
  # @param base_url [String] The base URL of the MCP server
45
49
  # @param headers [Hash] Additional headers to include in requests
@@ -138,6 +142,10 @@ module MCPClient
138
142
  # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
139
143
  # @raise [MCPClient::Errors::ToolCallError] for other errors during tool execution
140
144
  # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
145
+ # Call a tool with the given parameters
146
+ # @param tool_name [String] the name of the tool to call
147
+ # @param parameters [Hash] the parameters to pass to the tool
148
+ # @return [Object] the result of the tool invocation (with string keys for backward compatibility)
141
149
  def call_tool(tool_name, parameters)
142
150
  rpc_request('tools/call', {
143
151
  name: tool_name,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'mcp_client/json_rpc_common'
3
+ require_relative '../json_rpc_common'
4
4
 
5
5
  module MCPClient
6
6
  class ServerStdio
@@ -8,7 +8,7 @@ require 'logger'
8
8
  module MCPClient
9
9
  # JSON-RPC implementation of MCP server over stdio.
10
10
  class ServerStdio < ServerBase
11
- require 'mcp_client/server_stdio/json_rpc_transport'
11
+ require_relative 'server_stdio/json_rpc_transport'
12
12
 
13
13
  include JsonRpcTransport
14
14
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../http_transport_base'
4
+
5
+ module MCPClient
6
+ class ServerStreamableHTTP
7
+ # JSON-RPC request/notification plumbing for Streamable HTTP transport
8
+ # This transport uses HTTP POST requests but expects Server-Sent Event formatted responses
9
+ module JsonRpcTransport
10
+ include HttpTransportBase
11
+
12
+ private
13
+
14
+ # Log HTTP response for Streamable HTTP
15
+ # @param response [Faraday::Response] the HTTP response
16
+ def log_response(response)
17
+ @logger.debug("Received Streamable HTTP response: #{response.status} #{response.body}")
18
+ end
19
+
20
+ # Parse a Streamable HTTP JSON-RPC response (JSON or SSE format)
21
+ # @param response [Faraday::Response] the HTTP response
22
+ # @return [Hash] the parsed result
23
+ # @raise [MCPClient::Errors::TransportError] if parsing fails
24
+ # @raise [MCPClient::Errors::ServerError] if the response contains an error
25
+ def parse_response(response)
26
+ body = response.body.strip
27
+ content_type = response.headers['content-type'] || response.headers['Content-Type'] || ''
28
+
29
+ # Determine response format based on Content-Type header per MCP 2025 spec
30
+ data = if content_type.include?('text/event-stream')
31
+ # Parse SSE-formatted response for streaming
32
+ parse_sse_response(body)
33
+ else
34
+ # Parse regular JSON response (default for Streamable HTTP)
35
+ JSON.parse(body)
36
+ end
37
+
38
+ process_jsonrpc_response(data)
39
+ rescue JSON::ParserError => e
40
+ raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
41
+ end
42
+
43
+ # Parse Server-Sent Event formatted response with event ID tracking
44
+ # @param sse_body [String] the SSE formatted response body
45
+ # @return [Hash] the parsed JSON data
46
+ # @raise [MCPClient::Errors::TransportError] if no data found in SSE response
47
+ def parse_sse_response(sse_body)
48
+ # Extract JSON data and event ID from SSE format
49
+ # SSE format: event: message\nid: 123\ndata: {...}\n\n
50
+ data_lines = []
51
+ event_id = nil
52
+
53
+ sse_body.lines.each do |line|
54
+ line = line.strip
55
+ if line.start_with?('data:')
56
+ data_lines << line.sub(/^data:\s*/, '').strip
57
+ elsif line.start_with?('id:')
58
+ event_id = line.sub(/^id:\s*/, '').strip
59
+ end
60
+ end
61
+
62
+ raise MCPClient::Errors::TransportError, 'No data found in SSE response' if data_lines.empty?
63
+
64
+ # Track the last event ID for resumability
65
+ if event_id && !event_id.empty?
66
+ @last_event_id = event_id
67
+ @logger.debug("Tracking event ID for resumability: #{event_id}")
68
+ end
69
+
70
+ # Join multiline data fields according to SSE spec
71
+ json_data = data_lines.join("\n")
72
+ JSON.parse(json_data)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'json'
5
+ require 'monitor'
6
+ require 'logger'
7
+ require 'faraday'
8
+ require 'faraday/retry'
9
+
10
+ module MCPClient
11
+ # Implementation of MCP server that communicates via Streamable HTTP transport
12
+ # This transport uses HTTP POST requests but expects Server-Sent Event formatted responses
13
+ # It's designed for servers that support streaming responses over HTTP
14
+ class ServerStreamableHTTP < ServerBase
15
+ require_relative 'server_streamable_http/json_rpc_transport'
16
+
17
+ include JsonRpcTransport
18
+
19
+ # Default values for connection settings
20
+ DEFAULT_READ_TIMEOUT = 30
21
+ DEFAULT_MAX_RETRIES = 3
22
+
23
+ # @!attribute [r] base_url
24
+ # @return [String] The base URL of the MCP server
25
+ # @!attribute [r] endpoint
26
+ # @return [String] The JSON-RPC endpoint path
27
+ # @!attribute [r] tools
28
+ # @return [Array<MCPClient::Tool>, nil] List of available tools (nil if not fetched yet)
29
+ attr_reader :base_url, :endpoint, :tools
30
+
31
+ # Server information from initialize response
32
+ # @return [Hash, nil] Server information
33
+ attr_reader :server_info
34
+
35
+ # Server capabilities from initialize response
36
+ # @return [Hash, nil] Server capabilities
37
+ attr_reader :capabilities
38
+
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)
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
56
+
57
+ # Validate and normalize base_url
58
+ raise ArgumentError, "Invalid or insecure server URL: #{base_url}" unless valid_server_url?(base_url)
59
+
60
+ # Normalize base_url and handle cases where full endpoint is provided in base_url
61
+ uri = URI.parse(base_url.chomp('/'))
62
+
63
+ # Helper to build base URL without default ports
64
+ build_base_url = lambda do |parsed_uri|
65
+ port_part = if parsed_uri.port &&
66
+ !((parsed_uri.scheme == 'http' && parsed_uri.port == 80) ||
67
+ (parsed_uri.scheme == 'https' && parsed_uri.port == 443))
68
+ ":#{parsed_uri.port}"
69
+ else
70
+ ''
71
+ end
72
+ "#{parsed_uri.scheme}://#{parsed_uri.host}#{port_part}"
73
+ end
74
+
75
+ @base_url = build_base_url.call(uri)
76
+ @endpoint = if uri.path && !uri.path.empty? && uri.path != '/' && endpoint == '/rpc'
77
+ # If base_url contains a path and we're using default endpoint,
78
+ # treat the path as the endpoint and use the base URL without path
79
+ uri.path
80
+ else
81
+ # Standard case: base_url is just scheme://host:port, endpoint is separate
82
+ endpoint
83
+ end
84
+
85
+ # 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
95
+ @tools = nil
96
+ @tools_data = nil
97
+ @request_id = 0
98
+ @mutex = Monitor.new
99
+ @connection_established = false
100
+ @initialized = false
101
+ @http_conn = nil
102
+ @session_id = nil
103
+ @last_event_id = nil
104
+ end
105
+
106
+ # Connect to the MCP server over Streamable HTTP
107
+ # @return [Boolean] true if connection was successful
108
+ # @raise [MCPClient::Errors::ConnectionError] if connection fails
109
+ def connect
110
+ return true if @mutex.synchronize { @connection_established }
111
+
112
+ begin
113
+ @mutex.synchronize do
114
+ @connection_established = false
115
+ @initialized = false
116
+ end
117
+
118
+ # Test connectivity with a simple HTTP request
119
+ test_connection
120
+
121
+ # Perform MCP initialization handshake
122
+ perform_initialize
123
+
124
+ @mutex.synchronize do
125
+ @connection_established = true
126
+ @initialized = true
127
+ end
128
+
129
+ true
130
+ rescue MCPClient::Errors::ConnectionError => e
131
+ cleanup
132
+ raise e
133
+ rescue StandardError => e
134
+ cleanup
135
+ raise MCPClient::Errors::ConnectionError, "Failed to connect to MCP server at #{@base_url}: #{e.message}"
136
+ end
137
+ end
138
+
139
+ # List all tools available from the MCP server
140
+ # @return [Array<MCPClient::Tool>] list of available tools
141
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
142
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
143
+ # @raise [MCPClient::Errors::ToolCallError] for other errors during tool listing
144
+ def list_tools
145
+ @mutex.synchronize do
146
+ return @tools if @tools
147
+ end
148
+
149
+ begin
150
+ ensure_connected
151
+
152
+ tools_data = request_tools_list
153
+ @mutex.synchronize do
154
+ @tools = tools_data.map do |tool_data|
155
+ MCPClient::Tool.from_json(tool_data, server: self)
156
+ end
157
+ end
158
+
159
+ @mutex.synchronize { @tools }
160
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
161
+ # Re-raise these errors directly
162
+ raise
163
+ rescue StandardError => e
164
+ raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
165
+ end
166
+ end
167
+
168
+ # Call a tool with the given parameters
169
+ # @param tool_name [String] the name of the tool to call
170
+ # @param parameters [Hash] the parameters to pass to the tool
171
+ # @return [Object] the result of the tool invocation (with string keys for backward compatibility)
172
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
173
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
174
+ # @raise [MCPClient::Errors::ToolCallError] for other errors during tool execution
175
+ # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
176
+ def call_tool(tool_name, parameters)
177
+ rpc_request('tools/call', {
178
+ name: tool_name,
179
+ arguments: parameters
180
+ })
181
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
182
+ # Re-raise connection/transport errors directly to match test expectations
183
+ raise
184
+ rescue StandardError => e
185
+ # For all other errors, wrap in ToolCallError
186
+ raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
187
+ end
188
+
189
+ # Stream tool call (default implementation returns single-value stream)
190
+ # @param tool_name [String] the name of the tool to call
191
+ # @param parameters [Hash] the parameters to pass to the tool
192
+ # @return [Enumerator] stream of results
193
+ def call_tool_streaming(tool_name, parameters)
194
+ Enumerator.new do |yielder|
195
+ yielder << call_tool(tool_name, parameters)
196
+ end
197
+ end
198
+
199
+ # Override send_http_request to handle session headers for MCP protocol
200
+ def send_http_request(request)
201
+ conn = http_connection
202
+
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
213
+
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
219
+
220
+ req.body = request.to_json
221
+ end
222
+
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
239
+
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}"
249
+ end
250
+ end
251
+
252
+ # Terminate the current session (if any)
253
+ # @return [Boolean] true if termination was successful or no session exists
254
+ def terminate_session
255
+ @mutex.synchronize do
256
+ return true unless @session_id
257
+
258
+ super
259
+ end
260
+ end
261
+
262
+ # Clean up the server connection
263
+ # Properly closes HTTP connections and clears cached state
264
+ def cleanup
265
+ @mutex.synchronize do
266
+ # Attempt to terminate session before cleanup
267
+ terminate_session if @session_id
268
+
269
+ @connection_established = false
270
+ @initialized = false
271
+
272
+ @logger.debug('Cleaning up Streamable HTTP connection')
273
+
274
+ # Close HTTP connection if it exists
275
+ @http_conn = nil
276
+ @session_id = nil
277
+
278
+ @tools = nil
279
+ @tools_data = nil
280
+ end
281
+ end
282
+
283
+ private
284
+
285
+ # Test basic connectivity to the HTTP endpoint
286
+ # @return [void]
287
+ # @raise [MCPClient::Errors::ConnectionError] if connection test fails
288
+ def test_connection
289
+ create_http_connection
290
+
291
+ # Simple connectivity test - we'll use the actual initialize call
292
+ # since there's no standard HTTP health check endpoint
293
+ rescue Faraday::ConnectionFailed => e
294
+ raise MCPClient::Errors::ConnectionError, "Cannot connect to server at #{@base_url}: #{e.message}"
295
+ rescue Faraday::UnauthorizedError, Faraday::ForbiddenError => e
296
+ error_status = e.response ? e.response[:status] : 'unknown'
297
+ raise MCPClient::Errors::ConnectionError, "Authorization failed: HTTP #{error_status}"
298
+ rescue Faraday::Error => e
299
+ raise MCPClient::Errors::ConnectionError, "HTTP connection error: #{e.message}"
300
+ end
301
+
302
+ # Ensure connection is established
303
+ # @return [void]
304
+ # @raise [MCPClient::Errors::ConnectionError] if connection is not established
305
+ def ensure_connected
306
+ return if @mutex.synchronize { @connection_established && @initialized }
307
+
308
+ @logger.debug('Connection not active, attempting to reconnect before request')
309
+ cleanup
310
+ connect
311
+ end
312
+
313
+ # Request the tools list using JSON-RPC
314
+ # @return [Array<Hash>] the tools data
315
+ # @raise [MCPClient::Errors::ToolCallError] if tools list retrieval fails
316
+ def request_tools_list
317
+ @mutex.synchronize do
318
+ return @tools_data if @tools_data
319
+ end
320
+
321
+ result = rpc_request('tools/list')
322
+
323
+ if result.is_a?(Hash) && result['tools']
324
+ @mutex.synchronize do
325
+ @tools_data = result['tools']
326
+ end
327
+ return @mutex.synchronize { @tools_data.dup }
328
+ elsif result.is_a?(Array) || result
329
+ @mutex.synchronize do
330
+ @tools_data = result
331
+ end
332
+ return @mutex.synchronize { @tools_data.dup }
333
+ end
334
+
335
+ raise MCPClient::Errors::ToolCallError, 'Failed to get tools list from JSON-RPC request'
336
+ end
337
+ end
338
+ end
@@ -31,10 +31,11 @@ module MCPClient
31
31
  # @return [MCPClient::Tool] tool instance
32
32
  def self.from_json(data, server: nil)
33
33
  # Some servers (Playwright MCP CLI) use 'inputSchema' instead of 'schema'
34
- schema = data['inputSchema'] || data['schema']
34
+ # Handle both string and symbol keys
35
+ schema = data['inputSchema'] || data[:inputSchema] || data['schema'] || data[:schema]
35
36
  new(
36
- name: data['name'],
37
- description: data['description'],
37
+ name: data['name'] || data[:name],
38
+ description: data['description'] || data[:description],
38
39
  schema: schema,
39
40
  server: server
40
41
  )
@@ -2,8 +2,11 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.6.2'
5
+ VERSION = '0.7.0'
6
6
 
7
7
  # JSON-RPC handshake protocol version (date-based)
8
8
  PROTOCOL_VERSION = '2024-11-05'
9
+
10
+ # Protocol version for HTTP and Streamable HTTP transports
11
+ HTTP_PROTOCOL_VERSION = '2025-03-26'
9
12
  end
data/lib/mcp_client.rb CHANGED
@@ -6,6 +6,8 @@ require_relative 'mcp_client/tool'
6
6
  require_relative 'mcp_client/server_base'
7
7
  require_relative 'mcp_client/server_stdio'
8
8
  require_relative 'mcp_client/server_sse'
9
+ require_relative 'mcp_client/server_http'
10
+ require_relative 'mcp_client/server_streamable_http'
9
11
  require_relative 'mcp_client/server_factory'
10
12
  require_relative 'mcp_client/client'
11
13
  require_relative 'mcp_client/version'
@@ -33,7 +35,6 @@ module MCPClient
33
35
  parsed.each_value do |cfg|
34
36
  case cfg[:type].to_s
35
37
  when 'stdio'
36
- # Build command list with args and propagate environment
37
38
  cmd_list = [cfg[:command]] + Array(cfg[:args])
38
39
  configs << MCPClient.stdio_config(
39
40
  command: cmd_list,
@@ -42,9 +43,14 @@ module MCPClient
42
43
  env: cfg[:env]
43
44
  )
44
45
  when 'sse'
45
- # Use 'url' from parsed config as 'base_url' for SSE config
46
46
  configs << MCPClient.sse_config(base_url: cfg[:url], headers: cfg[:headers] || {}, name: cfg[:name],
47
47
  logger: logger)
48
+ when 'http'
49
+ configs << MCPClient.http_config(base_url: cfg[:url], endpoint: cfg[:endpoint],
50
+ headers: cfg[:headers] || {}, name: cfg[:name], logger: logger)
51
+ when 'streamable_http'
52
+ configs << MCPClient.streamable_http_config(base_url: cfg[:url], endpoint: cfg[:endpoint],
53
+ headers: cfg[:headers] || {}, name: cfg[:name], logger: logger)
48
54
  end
49
55
  end
50
56
  end
@@ -90,4 +96,55 @@ module MCPClient
90
96
  logger: logger
91
97
  }
92
98
  end
99
+
100
+ # Create a standard server configuration for HTTP
101
+ # @param base_url [String] base URL for the server
102
+ # @param endpoint [String] JSON-RPC endpoint path (default: '/rpc')
103
+ # @param headers [Hash] HTTP headers to include in requests
104
+ # @param read_timeout [Integer] read timeout in seconds (default: 30)
105
+ # @param retries [Integer] number of retry attempts (default: 3)
106
+ # @param retry_backoff [Integer] backoff delay in seconds (default: 1)
107
+ # @param name [String, nil] optional name for this server
108
+ # @param logger [Logger, nil] optional logger for server operations
109
+ # @return [Hash] server configuration
110
+ def self.http_config(base_url:, endpoint: '/rpc', headers: {}, read_timeout: 30, retries: 3, retry_backoff: 1,
111
+ name: nil, logger: nil)
112
+ {
113
+ type: 'http',
114
+ base_url: base_url,
115
+ endpoint: endpoint,
116
+ headers: headers,
117
+ read_timeout: read_timeout,
118
+ retries: retries,
119
+ retry_backoff: retry_backoff,
120
+ name: name,
121
+ logger: logger
122
+ }
123
+ end
124
+
125
+ # Create configuration for Streamable HTTP transport
126
+ # This transport uses HTTP POST requests but expects Server-Sent Event formatted responses
127
+ # @param base_url [String] Base URL of the MCP server
128
+ # @param endpoint [String] JSON-RPC endpoint path (default: '/rpc')
129
+ # @param headers [Hash] Additional headers to include in requests
130
+ # @param read_timeout [Integer] Read timeout in seconds (default: 30)
131
+ # @param retries [Integer] Number of retry attempts on transient errors (default: 3)
132
+ # @param retry_backoff [Integer] Backoff delay in seconds (default: 1)
133
+ # @param name [String, nil] Optional name for this server
134
+ # @param logger [Logger, nil] Optional logger for server operations
135
+ # @return [Hash] server configuration
136
+ def self.streamable_http_config(base_url:, endpoint: '/rpc', headers: {}, read_timeout: 30, retries: 3,
137
+ retry_backoff: 1, name: nil, logger: nil)
138
+ {
139
+ type: 'streamable_http',
140
+ base_url: base_url,
141
+ endpoint: endpoint,
142
+ headers: headers,
143
+ read_timeout: read_timeout,
144
+ retries: retries,
145
+ retry_backoff: retry_backoff,
146
+ name: name,
147
+ logger: logger
148
+ }
149
+ end
93
150
  end
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.6.2
4
+ version: 0.7.0
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-05-20 00:00:00.000000000 Z
11
+ date: 2025-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -108,15 +108,20 @@ files:
108
108
  - lib/mcp_client/client.rb
109
109
  - lib/mcp_client/config_parser.rb
110
110
  - lib/mcp_client/errors.rb
111
+ - lib/mcp_client/http_transport_base.rb
111
112
  - lib/mcp_client/json_rpc_common.rb
112
113
  - lib/mcp_client/server_base.rb
113
114
  - lib/mcp_client/server_factory.rb
115
+ - lib/mcp_client/server_http.rb
116
+ - lib/mcp_client/server_http/json_rpc_transport.rb
114
117
  - lib/mcp_client/server_sse.rb
115
118
  - lib/mcp_client/server_sse/json_rpc_transport.rb
116
119
  - lib/mcp_client/server_sse/reconnect_monitor.rb
117
120
  - lib/mcp_client/server_sse/sse_parser.rb
118
121
  - lib/mcp_client/server_stdio.rb
119
122
  - lib/mcp_client/server_stdio/json_rpc_transport.rb
123
+ - lib/mcp_client/server_streamable_http.rb
124
+ - lib/mcp_client/server_streamable_http/json_rpc_transport.rb
120
125
  - lib/mcp_client/tool.rb
121
126
  - lib/mcp_client/version.rb
122
127
  homepage: https://github.com/simonx1/ruby-mcp-client