ruby-mcp-client 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fd821eaf2b01a8628e7d54f2b39826b3d980d9e0d183ac0d81d5fb2ae135a36
4
- data.tar.gz: 0d2afee24c1ebbb49f597056b9257546256e3152a4d789b7bc2789b3afc40a4f
3
+ metadata.gz: 5249d2aeec436453cf92fb92a9d84468a3042d046d663d03eebffb734b16abd2
4
+ data.tar.gz: 30ece04171450241424fd81a95ef2f69d73e53300044d28d98796ecea88ba20b
5
5
  SHA512:
6
- metadata.gz: b21d59cb475b4202442658dd81b3ee394f317b767fb0a1bacba3c575dc96ac4004a0849a4fc59b2d7481c75ae55181cca21f11a5ebbe2d15179eeb2a30aba242
7
- data.tar.gz: 6a06a38dd0066331efcd1ec3a1eb5d6178dd12091a7f217199a314c891aef4b0100d3d7da9d12e3f69ac01da42d3eee60135f68ac43bf79cebf14f9fd43308e4
6
+ metadata.gz: 2c002ae531ff4ebfdb733638b9fe0a40ed4452a33bda9a7fac82b69395c662749b937f21d155bf9b9a6702a50c38bfa172c2ba59c3adad241a37495aa8d654d9
7
+ data.tar.gz: 4664e938db65412dc5aa7847dc7914b1e423d1f0a96e3dea65565138b494cb44b5449fd0be49be505e4170792a68e239b80da9a969fec9e620748b578a16c3c6
data/README.md CHANGED
@@ -29,7 +29,8 @@ MCP enables AI assistants and other services to discover and invoke external too
29
29
  via different transport mechanisms:
30
30
 
31
31
  - **Standard I/O**: Local processes implementing the MCP protocol
32
- - **Server-Sent Events (SSE)**: Remote MCP servers over HTTP
32
+ - **Server-Sent Events (SSE)**: Remote MCP servers over HTTP with streaming support
33
+ - **HTTP JSON-RPC**: Remote MCP servers over standard HTTP
33
34
 
34
35
  The core client resides in `MCPClient::Client` and provides helper methods for integrating
35
36
  with popular AI services with built-in conversions:
@@ -48,11 +49,19 @@ client = MCPClient.create_client(
48
49
  mcp_server_configs: [
49
50
  # Local stdio server
50
51
  MCPClient.stdio_config(command: 'npx -y @modelcontextprotocol/server-filesystem /home/user'),
51
- # Remote HTTP SSE server
52
+ # Remote HTTP SSE server (with streaming support)
52
53
  MCPClient.sse_config(
53
54
  base_url: 'https://api.example.com/sse',
54
55
  headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
55
56
  read_timeout: 30 # Optional timeout in seconds (default: 30)
57
+ ),
58
+ # Remote HTTP JSON-RPC server
59
+ MCPClient.http_config(
60
+ base_url: 'https://api.example.com/jsonrpc',
61
+ headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
62
+ read_timeout: 30, # Optional timeout in seconds (default: 30)
63
+ retries: 3, # Optional number of retry attempts (default: 0)
64
+ retry_backoff: 1 # Optional backoff delay in seconds (default: 1)
56
65
  )
57
66
  ]
58
67
  )
@@ -60,13 +69,31 @@ client = MCPClient.create_client(
60
69
  # List available tools
61
70
  tools = client.list_tools
62
71
 
72
+ # Find tools by name pattern (string or regex)
73
+ file_tools = client.find_tools('file')
74
+ first_tool = client.find_tool(/^file_/)
75
+
63
76
  # Call a specific tool by name
64
77
  result = client.call_tool('example_tool', { param1: 'value1', param2: 42 })
65
78
 
79
+ # Call multiple tools in batch
80
+ results = client.call_tools([
81
+ { name: 'tool1', parameters: { key1: 'value1' } },
82
+ { name: 'tool2', parameters: { key2: 'value2' } }
83
+ ])
84
+
85
+ # Stream results (for supported transports like SSE)
86
+ client.call_tool_streaming('streaming_tool', { param: 'value' }).each do |chunk|
87
+ # Process each chunk as it arrives
88
+ puts chunk
89
+ end
90
+
66
91
  # Format tools for specific AI services
67
92
  openai_tools = client.to_openai_tools
68
93
  anthropic_tools = client.to_anthropic_tools
69
94
 
95
+ # Clear cached tools to force fresh fetch on next list
96
+ client.clear_cache
70
97
  # Clean up connections
71
98
  client.cleanup
72
99
  ```
@@ -166,6 +193,8 @@ This client works with any MCP-compatible server, including:
166
193
  - [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
167
194
  - Custom servers implementing the MCP protocol
168
195
 
196
+ ### Server Implementation Features
197
+
169
198
  ### Server-Sent Events (SSE) Implementation
170
199
 
171
200
  The SSE client implementation provides these key features:
@@ -174,6 +203,16 @@ The SSE client implementation provides these key features:
174
203
  - **Thread safety**: All operations are thread-safe using monitors and synchronized access
175
204
  - **Reliable error handling**: Comprehensive error handling for network issues, timeouts, and malformed responses
176
205
  - **JSON-RPC over SSE**: Full implementation of JSON-RPC 2.0 over SSE transport
206
+ - **Streaming support**: Native streaming for real-time updates
207
+
208
+ ### HTTP JSON-RPC Implementation
209
+
210
+ The HTTP client implementation provides these key features:
211
+
212
+ - **Resilient connection handling**: Manages HTTP/HTTPS connections with configurable timeouts
213
+ - **Retry mechanism**: Configurable retry attempts with exponential backoff for transient errors
214
+ - **Error handling**: Comprehensive error handling for network issues, timeouts, and malformed responses
215
+ - **JSON-RPC over HTTP**: Standard JSON-RPC 2.0 implementation
177
216
 
178
217
  ## Requirements
179
218
 
@@ -1,13 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
4
+
3
5
  module MCPClient
4
6
  # MCP Client for integrating with the Model Context Protocol
5
7
  # This is the main entry point for using MCP tools
6
8
  class Client
7
- attr_reader :servers, :tool_cache
9
+ # @!attribute [r] servers
10
+ # @return [Array<MCPClient::ServerBase>] list of servers
11
+ # @!attribute [r] tool_cache
12
+ # @return [Hash<String, MCPClient::Tool>] cache of tools by name
13
+ # @!attribute [r] logger
14
+ # @return [Logger] logger for client operations
15
+ attr_reader :servers, :tool_cache, :logger
8
16
 
9
- def initialize(mcp_server_configs: [])
10
- @servers = mcp_server_configs.map { |config| MCPClient::ServerFactory.create(config) }
17
+ # Initialize a new MCPClient::Client
18
+ # @param mcp_server_configs [Array<Hash>] configurations for MCP servers
19
+ # @param logger [Logger, nil] optional logger, defaults to STDOUT
20
+ def initialize(mcp_server_configs: [], logger: nil)
21
+ @logger = logger || Logger.new($stdout, level: Logger::WARN)
22
+ @servers = mcp_server_configs.map do |config|
23
+ @logger.debug("Creating server with config: #{config.inspect}")
24
+ MCPClient::ServerFactory.create(config)
25
+ end
11
26
  @tool_cache = {}
12
27
  end
13
28
 
@@ -38,6 +53,9 @@ module MCPClient
38
53
 
39
54
  raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found" unless tool
40
55
 
56
+ # Validate parameters against tool schema
57
+ validate_params!(tool, parameters)
58
+
41
59
  # Find the server that owns this tool
42
60
  server = find_server_for_tool(tool)
43
61
  raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server
@@ -46,15 +64,21 @@ module MCPClient
46
64
  end
47
65
 
48
66
  # Convert MCP tools to OpenAI function specifications
67
+ # @param tool_names [Array<String>, nil] optional list of tool names to include, nil means all tools
49
68
  # @return [Array<Hash>] OpenAI function specifications
50
- def to_openai_tools
51
- list_tools.map(&:to_openai_tool)
69
+ def to_openai_tools(tool_names: nil)
70
+ tools = list_tools
71
+ tools = tools.select { |t| tool_names.include?(t.name) } if tool_names
72
+ tools.map(&:to_openai_tool)
52
73
  end
53
74
 
54
75
  # Convert MCP tools to Anthropic Claude tool specifications
76
+ # @param tool_names [Array<String>, nil] optional list of tool names to include, nil means all tools
55
77
  # @return [Array<Hash>] Anthropic Claude tool specifications
56
- def to_anthropic_tools
57
- list_tools.map(&:to_anthropic_tool)
78
+ def to_anthropic_tools(tool_names: nil)
79
+ tools = list_tools
80
+ tools = tools.select { |t| tool_names.include?(t.name) } if tool_names
81
+ tools.map(&:to_anthropic_tool)
58
82
  end
59
83
 
60
84
  # Clean up all server connections
@@ -62,8 +86,82 @@ module MCPClient
62
86
  servers.each(&:cleanup)
63
87
  end
64
88
 
89
+ # Clear the cached tools so that next list_tools will fetch fresh data
90
+ # @return [void]
91
+ def clear_cache
92
+ @tool_cache.clear
93
+ end
94
+
95
+ # Find all tools whose name matches the given pattern (String or Regexp)
96
+ # @param pattern [String, Regexp] pattern to match tool names
97
+ # @return [Array<MCPClient::Tool>] matching tools
98
+ def find_tools(pattern)
99
+ rx = pattern.is_a?(Regexp) ? pattern : /#{Regexp.escape(pattern)}/
100
+ list_tools.select { |t| t.name.match(rx) }
101
+ end
102
+
103
+ # Find the first tool whose name matches the given pattern
104
+ # @param pattern [String, Regexp] pattern to match tool names
105
+ # @return [MCPClient::Tool, nil]
106
+ def find_tool(pattern)
107
+ find_tools(pattern).first
108
+ end
109
+
110
+ # Call multiple tools in batch
111
+ # @param calls [Array<Hash>] array of calls in the form { name: tool_name, parameters: {...} }
112
+ # @return [Array<Object>] array of results for each tool invocation
113
+ def call_tools(calls)
114
+ calls.map do |call|
115
+ name = call[:name] || call['name']
116
+ params = call[:parameters] || call['parameters'] || {}
117
+ call_tool(name, params)
118
+ end
119
+ end
120
+
121
+ # Stream call of a specific tool by name with the given parameters.
122
+ # Returns an Enumerator yielding streaming updates if supported.
123
+ # @param tool_name [String] the name of the tool to call
124
+ # @param parameters [Hash] the parameters to pass to the tool
125
+ # @return [Enumerator] streaming enumerator or single-value enumerator
126
+ def call_tool_streaming(tool_name, parameters)
127
+ tools = list_tools
128
+ tool = tools.find { |t| t.name == tool_name }
129
+ raise MCPClient::Errors::ToolNotFound, "Tool '#{tool_name}' not found" unless tool
130
+
131
+ # Validate parameters against tool schema
132
+ validate_params!(tool, parameters)
133
+ # Find the server that owns this tool
134
+ server = find_server_for_tool(tool)
135
+ raise MCPClient::Errors::ServerNotFound, "No server found for tool '#{tool_name}'" unless server
136
+
137
+ if server.respond_to?(:call_tool_streaming)
138
+ server.call_tool_streaming(tool_name, parameters)
139
+ else
140
+ Enumerator.new do |yielder|
141
+ yielder << server.call_tool(tool_name, parameters)
142
+ end
143
+ end
144
+ end
145
+
65
146
  private
66
147
 
148
+ # Validate parameters against tool JSON schema (checks required properties)
149
+ # @param tool [MCPClient::Tool] tool definition with schema
150
+ # @param parameters [Hash] parameters to validate
151
+ # @raise [MCPClient::Errors::ValidationError] when required params are missing
152
+ def validate_params!(tool, parameters)
153
+ schema = tool.schema
154
+ return unless schema.is_a?(Hash)
155
+
156
+ required = schema['required'] || schema[:required]
157
+ return unless required.is_a?(Array)
158
+
159
+ missing = required.map(&:to_s) - parameters.keys.map(&:to_s)
160
+ return unless missing.any?
161
+
162
+ raise MCPClient::Errors::ValidationError, "Missing required parameters: #{missing.join(', ')}"
163
+ end
164
+
67
165
  def find_server_for_tool(tool)
68
166
  servers.find do |server|
69
167
  server.list_tools.any? { |t| t.name == tool.name }
@@ -23,5 +23,7 @@ module MCPClient
23
23
 
24
24
  # Raised when there's an error in the MCP server transport
25
25
  class TransportError < MCPError; end
26
+ # Raised when tool parameters fail validation against JSON schema
27
+ class ValidationError < MCPError; end
26
28
  end
27
29
  end
@@ -14,7 +14,19 @@ module MCPClient
14
14
  MCPClient::ServerSSE.new(
15
15
  base_url: config[:base_url],
16
16
  headers: config[:headers] || {},
17
- read_timeout: config[:read_timeout] || 30
17
+ read_timeout: config[:read_timeout] || 30,
18
+ retries: config[:retries] || 0,
19
+ retry_backoff: config[:retry_backoff] || 1,
20
+ logger: config[:logger]
21
+ )
22
+ when 'http'
23
+ MCPClient::ServerHTTP.new(
24
+ base_url: config[:base_url],
25
+ headers: config[:headers] || {},
26
+ read_timeout: config[:read_timeout] || 30,
27
+ retries: config[:retries] || 0,
28
+ retry_backoff: config[:retry_backoff] || 1,
29
+ logger: config[:logger]
18
30
  )
19
31
  else
20
32
  raise ArgumentError, "Unknown server type: #{config[:type]}"
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'json'
6
+ require 'openssl'
7
+ require 'logger'
8
+
9
+ module MCPClient
10
+ # Implementation of MCP server over HTTP JSON-RPC
11
+ class ServerHTTP < ServerBase
12
+ attr_reader :base_url, :headers, :read_timeout, :max_retries, :retry_backoff, :logger
13
+
14
+ # @param base_url [String] The base URL of the MCP HTTP server
15
+ # @param headers [Hash] HTTP headers to include in requests
16
+ # @param read_timeout [Integer] Read timeout in seconds
17
+ # @param retries [Integer] number of retry attempts on transient errors
18
+ # @param retry_backoff [Numeric] base delay in seconds for exponential backoff
19
+ # @param logger [Logger, nil] optional logger
20
+ def initialize(base_url:, headers: {}, read_timeout: 30, retries: 0, retry_backoff: 1, logger: nil)
21
+ super()
22
+ @base_url = base_url
23
+ @headers = headers
24
+ @read_timeout = read_timeout
25
+ @max_retries = retries
26
+ @retry_backoff = retry_backoff
27
+ @logger = logger || Logger.new($stdout, level: Logger::WARN)
28
+ @request_id = 0
29
+ end
30
+
31
+ # List available tools
32
+ # @return [Array<MCPClient::Tool>]
33
+ def list_tools
34
+ request_json = jsonrpc_request('tools/list', {})
35
+ result = send_request(request_json)
36
+ (result['tools'] || []).map { |td| MCPClient::Tool.from_json(td) }
37
+ rescue MCPClient::Errors::MCPError
38
+ raise
39
+ rescue StandardError => e
40
+ raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
41
+ end
42
+
43
+ # Call a tool with given parameters
44
+ # @param tool_name [String]
45
+ # @param parameters [Hash]
46
+ # @return [Object] result of invocation
47
+ def call_tool(tool_name, parameters)
48
+ request_json = jsonrpc_request('tools/call', { 'name' => tool_name, 'arguments' => parameters })
49
+ send_request(request_json)
50
+ rescue MCPClient::Errors::MCPError
51
+ raise
52
+ rescue StandardError => e
53
+ raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
54
+ end
55
+
56
+ # Streaming is not supported over simple HTTP transport; fallback to single response
57
+ # @param tool_name [String]
58
+ # @param parameters [Hash]
59
+ # @return [Enumerator]
60
+ def call_tool_streaming(tool_name, parameters)
61
+ Enumerator.new do |yielder|
62
+ yielder << call_tool(tool_name, parameters)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def jsonrpc_request(method, params)
69
+ @request_id += 1
70
+ {
71
+ 'jsonrpc' => '2.0',
72
+ 'id' => @request_id,
73
+ 'method' => method,
74
+ 'params' => params
75
+ }
76
+ end
77
+
78
+ def send_request(request)
79
+ attempts = 0
80
+ begin
81
+ attempts += 1
82
+ uri = URI.parse(base_url)
83
+ http = Net::HTTP.new(uri.host, uri.port)
84
+ if uri.scheme == 'https'
85
+ http.use_ssl = true
86
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
87
+ end
88
+ http.open_timeout = 10
89
+ http.read_timeout = read_timeout
90
+
91
+ @logger.debug("Sending HTTP JSONRPC request: #{request.to_json}")
92
+ response = http.post(uri.path, request.to_json, default_headers)
93
+ @logger.debug("Received HTTP response: #{response.code} #{response.body}")
94
+
95
+ unless response.is_a?(Net::HTTPSuccess)
96
+ raise MCPClient::Errors::ServerError, "Server returned error: #{response.code} #{response.message}"
97
+ end
98
+
99
+ data = JSON.parse(response.body)
100
+ raise MCPClient::Errors::ServerError, data['error']['message'] if data['error']
101
+
102
+ data['result']
103
+ rescue MCPClient::Errors::ServerError, MCPClient::Errors::TransportError, IOError, Timeout::Error => e
104
+ raise unless attempts <= max_retries
105
+
106
+ delay = retry_backoff * (2**(attempts - 1))
107
+ @logger.debug("Retry attempt #{attempts} after error: #{e.message}, sleeping #{delay}s")
108
+ sleep(delay)
109
+ retry
110
+ rescue JSON::ParserError => e
111
+ raise MCPClient::Errors::TransportError, "Invalid JSON response: #{e.message}"
112
+ end
113
+ end
114
+
115
+ def default_headers
116
+ h = headers.dup
117
+ h['Content-Type'] = 'application/json'
118
+ h
119
+ end
120
+ end
121
+ end
@@ -5,6 +5,7 @@ require 'net/http'
5
5
  require 'json'
6
6
  require 'openssl'
7
7
  require 'monitor'
8
+ require 'logger'
8
9
 
9
10
  module MCPClient
10
11
  # Implementation of MCP server that communicates via Server-Sent Events (SSE)
@@ -15,8 +16,14 @@ module MCPClient
15
16
  # @param base_url [String] The base URL of the MCP server
16
17
  # @param headers [Hash] Additional headers to include in requests
17
18
  # @param read_timeout [Integer] Read timeout in seconds (default: 30)
18
- def initialize(base_url:, headers: {}, read_timeout: 30)
19
+ # @param retries [Integer] number of retry attempts on transient errors
20
+ # @param retry_backoff [Numeric] base delay in seconds for exponential backoff
21
+ # @param logger [Logger, nil] optional logger
22
+ def initialize(base_url:, headers: {}, read_timeout: 30, retries: 0, retry_backoff: 1, logger: nil)
19
23
  super()
24
+ @logger = logger || Logger.new($stdout, level: Logger::WARN)
25
+ @max_retries = retries
26
+ @retry_backoff = retry_backoff
20
27
  @base_url = base_url.end_with?('/') ? base_url : "#{base_url}/"
21
28
  @headers = headers.merge({
22
29
  'Accept' => 'text/event-stream',
@@ -37,6 +44,16 @@ module MCPClient
37
44
  @connection_cv = @mutex.new_cond
38
45
  end
39
46
 
47
+ # Stream tool call fallback for SSE transport (yields single result)
48
+ # @param tool_name [String]
49
+ # @param parameters [Hash]
50
+ # @return [Enumerator]
51
+ def call_tool_streaming(tool_name, parameters)
52
+ Enumerator.new do |yielder|
53
+ yielder << call_tool(tool_name, parameters)
54
+ end
55
+ end
56
+
40
57
  # List all tools available from the MCP server
41
58
  # @return [Array<MCPClient::Tool>] list of available tools
42
59
  # @raise [MCPClient::Errors::ServerError] if server returns an error
@@ -201,6 +218,7 @@ module MCPClient
201
218
  end
202
219
 
203
220
  response.read_body do |chunk|
221
+ @logger.debug("SSE chunk received: #{chunk.inspect}")
204
222
  process_sse_chunk(chunk.dup)
205
223
  end
206
224
  end
@@ -219,6 +237,7 @@ module MCPClient
219
237
  # Process an SSE chunk from the server
220
238
  # @param chunk [String] the chunk to process
221
239
  def process_sse_chunk(chunk)
240
+ @logger.debug("Processing SSE chunk: #{chunk.inspect}")
222
241
  local_buffer = nil
223
242
 
224
243
  @mutex.synchronize do
@@ -278,6 +297,7 @@ module MCPClient
278
297
  # @param event_data [String] the event data to parse
279
298
  # @return [Hash, nil] the parsed event, or nil if the event is invalid
280
299
  def parse_sse_event(event_data)
300
+ @logger.debug("Parsing SSE event data: #{event_data.inspect}")
281
301
  event = { event: 'message', data: '', id: nil }
282
302
  data_lines = []
283
303
 
@@ -295,6 +315,7 @@ module MCPClient
295
315
  end
296
316
 
297
317
  event[:data] = data_lines.join("\n")
318
+ @logger.debug("Parsed SSE event: #{event.inspect}")
298
319
  event[:data].empty? ? nil : event
299
320
  end
300
321
 
@@ -331,10 +352,31 @@ module MCPClient
331
352
  raise MCPClient::Errors::ToolCallError, 'Failed to get tools list from JSON-RPC request'
332
353
  end
333
354
 
355
+ # Helper: execute block with retry/backoff for transient errors
356
+ # @yield block to execute
357
+ # @return result of block
358
+ def with_retry
359
+ attempts = 0
360
+ begin
361
+ yield
362
+ rescue MCPClient::Errors::TransportError, MCPClient::Errors::ServerError, IOError, Errno::ETIMEDOUT,
363
+ Errno::ECONNRESET => e
364
+ attempts += 1
365
+ if attempts <= @max_retries
366
+ delay = @retry_backoff * (2**(attempts - 1))
367
+ @logger.debug("Retry attempt #{attempts} after error: #{e.message}, sleeping #{delay}s")
368
+ sleep(delay)
369
+ retry
370
+ end
371
+ raise
372
+ end
373
+ end
374
+
334
375
  # Send a JSON-RPC request to the server and wait for result
335
376
  # @param request [Hash] the JSON-RPC request
336
377
  # @return [Hash] the result of the request
337
378
  def send_jsonrpc_request(request)
379
+ @logger.debug("Sending JSON-RPC request: #{request.to_json}")
338
380
  uri = URI.parse(@base_url)
339
381
  rpc_http = Net::HTTP.new(uri.host, uri.port)
340
382
 
@@ -367,6 +409,7 @@ module MCPClient
367
409
  .each { |k, v| http_request[k] = v }
368
410
 
369
411
  response = http.request(http_request)
412
+ @logger.debug("Received JSON-RPC response: #{response.code} #{response.body}")
370
413
 
371
414
  unless response.is_a?(Net::HTTPSuccess)
372
415
  raise MCPClient::Errors::ServerError, "Server returned error: #{response.code} #{response.message}"
@@ -3,6 +3,7 @@
3
3
  require 'open3'
4
4
  require 'json'
5
5
  require_relative 'version'
6
+ require 'logger'
6
7
 
7
8
  module MCPClient
8
9
  # JSON-RPC implementation of MCP server over stdio.
@@ -13,7 +14,10 @@ module MCPClient
13
14
  READ_TIMEOUT = 15
14
15
 
15
16
  # @param command [String, Array] the stdio command to launch the MCP JSON-RPC server
16
- def initialize(command:)
17
+ # @param retries [Integer] number of retry attempts on transient errors
18
+ # @param retry_backoff [Numeric] base delay in seconds for exponential backoff
19
+ # @param logger [Logger, nil] optional logger
20
+ def initialize(command:, retries: 0, retry_backoff: 1, logger: nil)
17
21
  super()
18
22
  @command = command.is_a?(Array) ? command.join(' ') : command
19
23
  @mutex = Mutex.new
@@ -21,6 +25,9 @@ module MCPClient
21
25
  @next_id = 1
22
26
  @pending = {}
23
27
  @initialized = false
28
+ @logger = logger || Logger.new($stdout, level: Logger::WARN)
29
+ @max_retries = retries
30
+ @retry_backoff = retry_backoff
24
31
  end
25
32
 
26
33
  # Connect to the MCP server by launching the command process via stdout/stdin
@@ -49,6 +56,7 @@ module MCPClient
49
56
  # @param line [String] line of output to parse
50
57
  def handle_line(line)
51
58
  msg = JSON.parse(line)
59
+ @logger.debug("Received line: #{line.chomp}")
52
60
  id = msg['id']
53
61
  return unless id
54
62
 
@@ -173,6 +181,7 @@ module MCPClient
173
181
  end
174
182
 
175
183
  def send_request(req)
184
+ @logger.debug("Sending JSONRPC request: #{req.to_json}")
176
185
  @stdin.puts(req.to_json)
177
186
  rescue StandardError => e
178
187
  raise MCPClient::Errors::TransportError, "Failed to send JSONRPC request: #{e.message}"
@@ -194,5 +203,15 @@ module MCPClient
194
203
  msg
195
204
  end
196
205
  end
206
+
207
+ # Stream tool call fallback for stdio transport (yields single result)
208
+ # @param tool_name [String]
209
+ # @param parameters [Hash]
210
+ # @return [Enumerator]
211
+ def call_tool_streaming(tool_name, parameters)
212
+ Enumerator.new do |yielder|
213
+ yielder << call_tool(tool_name, parameters)
214
+ end
215
+ end
197
216
  end
198
217
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
data/lib/mcp_client.rb CHANGED
@@ -6,6 +6,7 @@ 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'
9
10
  require_relative 'mcp_client/server_factory'
10
11
  require_relative 'mcp_client/client'
11
12
  require_relative 'mcp_client/version'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-mcp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Szymon Kurcab
@@ -81,6 +81,7 @@ files:
81
81
  - lib/mcp_client/errors.rb
82
82
  - lib/mcp_client/server_base.rb
83
83
  - lib/mcp_client/server_factory.rb
84
+ - lib/mcp_client/server_http.rb
84
85
  - lib/mcp_client/server_sse.rb
85
86
  - lib/mcp_client/server_stdio.rb
86
87
  - lib/mcp_client/tool.rb