ruby-mcp-client 0.4.1 → 0.5.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 +4 -4
- data/README.md +74 -6
- data/lib/mcp_client/client.rb +73 -8
- data/lib/mcp_client/server_base.rb +3 -4
- data/lib/mcp_client/server_sse.rb +97 -156
- data/lib/mcp_client/version.rb +1 -1
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79a86302428257274c4e620fae0b7ad45c6229cf381a956ef3b0a7e736f17f62
|
4
|
+
data.tar.gz: 80727b0aa48a992054dafd7c484eb611404b47d878ef2869e59e937a815718dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 977acc1ae8ca48a17b7827f006ed82578eb7bc24bf9ba08b03e77493e3447febc82287242a410861fb675b1fb74b0b67525dc4f92c9f8b448cd1cad2e3afc875
|
7
|
+
data.tar.gz: f940ed03a52065a5d8687d1722a588693d69a9c69f820771b328fed3b1bb69068fc7110dbedc745ea4fc42d047afa8afe975b2ed821aa5d33ac8c4e25fac9da7
|
data/README.md
CHANGED
@@ -100,9 +100,8 @@ result = client.send_rpc('another_method', params: { data: 123 }) # Uses first a
|
|
100
100
|
client.send_notification('status_update', params: { status: 'ready' })
|
101
101
|
|
102
102
|
# Check server connectivity
|
103
|
-
client.ping # Basic connectivity check
|
104
|
-
client.ping(
|
105
|
-
client.ping({}, server_index: 1) # Ping a specific server by index
|
103
|
+
client.ping # Basic connectivity check (zero-parameter heartbeat call)
|
104
|
+
client.ping(server_index: 1) # Ping a specific server by index
|
106
105
|
|
107
106
|
# Clear cached tools to force fresh fetch on next list
|
108
107
|
client.clear_cache
|
@@ -110,6 +109,61 @@ client.clear_cache
|
|
110
109
|
client.cleanup
|
111
110
|
```
|
112
111
|
|
112
|
+
### Server-Sent Events (SSE) Example
|
113
|
+
|
114
|
+
The SSE transport provides robust connection handling for remote MCP servers:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
require 'mcp_client'
|
118
|
+
require 'logger'
|
119
|
+
|
120
|
+
# Optional logger for debugging
|
121
|
+
logger = Logger.new($stdout)
|
122
|
+
logger.level = Logger::INFO
|
123
|
+
|
124
|
+
# Create an MCP client that connects to a Playwright MCP server via SSE
|
125
|
+
# First run: npx @playwright/mcp@latest --port 8931
|
126
|
+
sse_client = MCPClient.create_client(
|
127
|
+
mcp_server_configs: [
|
128
|
+
MCPClient.sse_config(
|
129
|
+
base_url: 'http://localhost:8931/sse',
|
130
|
+
read_timeout: 30, # Timeout in seconds
|
131
|
+
)
|
132
|
+
]
|
133
|
+
)
|
134
|
+
|
135
|
+
# List available tools
|
136
|
+
tools = sse_client.list_tools
|
137
|
+
|
138
|
+
# Launch a browser
|
139
|
+
result = sse_client.call_tool('browser_install', {})
|
140
|
+
result = sse_client.call_tool('browser_navigate', { url: 'about:blank' })
|
141
|
+
# No browser ID needed with these tool names
|
142
|
+
|
143
|
+
# Create a new page
|
144
|
+
page_result = sse_client.call_tool('browser_tab_new', {})
|
145
|
+
# No page ID needed with these tool names
|
146
|
+
|
147
|
+
# Navigate to a website
|
148
|
+
sse_client.call_tool('browser_navigate', { url: 'https://example.com' })
|
149
|
+
|
150
|
+
# Get page title
|
151
|
+
title_result = sse_client.call_tool('browser_snapshot', {})
|
152
|
+
puts "Page snapshot: #{title_result}"
|
153
|
+
|
154
|
+
# Take a screenshot
|
155
|
+
screenshot_result = sse_client.call_tool('browser_take_screenshot', {})
|
156
|
+
|
157
|
+
# Ping the server to verify connectivity
|
158
|
+
ping_result = sse_client.ping
|
159
|
+
puts "Ping successful: #{ping_result.inspect}"
|
160
|
+
|
161
|
+
# Clean up
|
162
|
+
sse_client.cleanup
|
163
|
+
```
|
164
|
+
|
165
|
+
See `examples/mcp_sse_server_example.rb` for the full Playwright SSE example.
|
166
|
+
|
113
167
|
### Integration Examples
|
114
168
|
|
115
169
|
The repository includes examples for integrating with popular AI APIs:
|
@@ -196,6 +250,7 @@ Complete examples can be found in the `examples/` directory:
|
|
196
250
|
- `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
|
197
251
|
- `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
|
198
252
|
- `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
|
253
|
+
- `mcp_sse_server_example.rb` - SSE transport with Playwright MCP
|
199
254
|
|
200
255
|
## MCP Server Compatibility
|
201
256
|
|
@@ -205,7 +260,20 @@ This client works with any MCP-compatible server, including:
|
|
205
260
|
- [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
|
206
261
|
- Custom servers implementing the MCP protocol
|
207
262
|
|
208
|
-
|
263
|
+
## Key Features
|
264
|
+
|
265
|
+
### Client Features
|
266
|
+
|
267
|
+
- **Multiple transports** - Support for both stdio and SSE transports
|
268
|
+
- **Multiple servers** - Connect to multiple MCP servers simultaneously
|
269
|
+
- **Tool discovery** - Find tools by name or pattern
|
270
|
+
- **Atomic tool calls** - Simple API for invoking tools with parameters
|
271
|
+
- **Batch support** - Call multiple tools in a single operation
|
272
|
+
- **API conversions** - Built-in format conversion for OpenAI and Anthropic APIs
|
273
|
+
- **Thread safety** - Synchronized access for thread-safe operation
|
274
|
+
- **Server notifications** - Support for JSON-RPC notifications
|
275
|
+
- **Custom RPC methods** - Send any custom JSON-RPC method
|
276
|
+
- **Consistent error handling** - Rich error types for better exception handling
|
209
277
|
|
210
278
|
### Server-Sent Events (SSE) Implementation
|
211
279
|
|
@@ -226,7 +294,7 @@ The SSE client implementation provides these key features:
|
|
226
294
|
|
227
295
|
## Requirements
|
228
296
|
|
229
|
-
- Ruby >= 2.
|
297
|
+
- Ruby >= 3.2.0
|
230
298
|
- No runtime dependencies
|
231
299
|
|
232
300
|
## Implementing an MCP Server
|
@@ -274,4 +342,4 @@ This gem is available as open source under the [MIT License](LICENSE).
|
|
274
342
|
## Contributing
|
275
343
|
|
276
344
|
Bug reports and pull requests are welcome on GitHub at
|
277
|
-
https://github.com/simonx1/ruby-mcp-client.
|
345
|
+
https://github.com/simonx1/ruby-mcp-client.
|
data/lib/mcp_client/client.rb
CHANGED
@@ -31,9 +31,9 @@ module MCPClient
|
|
31
31
|
# Register default and user-defined notification handlers on each server
|
32
32
|
@servers.each do |server|
|
33
33
|
server.on_notification do |method, params|
|
34
|
-
# Default
|
35
|
-
|
36
|
-
# Invoke user listeners
|
34
|
+
# Default notification processing (e.g., cache invalidation, logging)
|
35
|
+
process_notification(server, method, params)
|
36
|
+
# Invoke user-defined listeners
|
37
37
|
@notification_listeners.each { |cb| cb.call(server, method, params) }
|
38
38
|
end
|
39
39
|
end
|
@@ -163,17 +163,16 @@ module MCPClient
|
|
163
163
|
end
|
164
164
|
end
|
165
165
|
|
166
|
-
# Ping the MCP server to check connectivity
|
167
|
-
# @param params [Hash] optional parameters for the ping request
|
166
|
+
# Ping the MCP server to check connectivity (zero-parameter heartbeat call)
|
168
167
|
# @param server_index [Integer, nil] optional index of a specific server to ping, nil for first available
|
169
168
|
# @return [Object] result from the ping request
|
170
169
|
# @raise [MCPClient::Errors::ServerNotFound] if no server is available
|
171
|
-
def ping(
|
170
|
+
def ping(server_index: nil)
|
172
171
|
if server_index.nil?
|
173
172
|
# Ping first available server
|
174
173
|
raise MCPClient::Errors::ServerNotFound, 'No server available for ping' if @servers.empty?
|
175
174
|
|
176
|
-
@servers.first.ping
|
175
|
+
@servers.first.ping
|
177
176
|
else
|
178
177
|
# Ping specified server
|
179
178
|
if server_index >= @servers.length
|
@@ -181,12 +180,78 @@ module MCPClient
|
|
181
180
|
"Server at index #{server_index} not found"
|
182
181
|
end
|
183
182
|
|
184
|
-
@servers[server_index].ping
|
183
|
+
@servers[server_index].ping
|
185
184
|
end
|
186
185
|
end
|
187
186
|
|
187
|
+
# Send a raw JSON-RPC request to a server
|
188
|
+
# @param method [String] JSON-RPC method name
|
189
|
+
# @param params [Hash] parameters for the request
|
190
|
+
# @param server [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
|
191
|
+
# @return [Object] result from the JSON-RPC response
|
192
|
+
def send_rpc(method, params: {}, server: nil)
|
193
|
+
srv = select_server(server)
|
194
|
+
srv.rpc_request(method, params)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Send a raw JSON-RPC notification to a server (no response expected)
|
198
|
+
# @param method [String] JSON-RPC method name
|
199
|
+
# @param params [Hash] parameters for the notification
|
200
|
+
# @param server [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
|
201
|
+
# @return [void]
|
202
|
+
def send_notification(method, params: {}, server: nil)
|
203
|
+
srv = select_server(server)
|
204
|
+
srv.rpc_notify(method, params)
|
205
|
+
end
|
206
|
+
|
188
207
|
private
|
189
208
|
|
209
|
+
# Process incoming JSON-RPC notifications with default handlers
|
210
|
+
# @param server [MCPClient::ServerBase] the server that emitted the notification
|
211
|
+
# @param method [String] JSON-RPC notification method
|
212
|
+
# @param params [Hash] parameters for the notification
|
213
|
+
# @return [void]
|
214
|
+
def process_notification(server, method, params)
|
215
|
+
case method
|
216
|
+
when 'notifications/tools/list_changed'
|
217
|
+
logger.warn("[#{server.class}] Tool list has changed, clearing tool cache")
|
218
|
+
clear_cache
|
219
|
+
when 'notifications/resources/updated'
|
220
|
+
logger.warn("[#{server.class}] Resource #{params['uri']} updated")
|
221
|
+
when 'notifications/prompts/list_changed'
|
222
|
+
logger.warn("[#{server.class}] Prompt list has changed")
|
223
|
+
when 'notifications/resources/list_changed'
|
224
|
+
logger.warn("[#{server.class}] Resource list has changed")
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Select a server based on index, type, or instance
|
229
|
+
# @param server_arg [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
|
230
|
+
# @return [MCPClient::ServerBase]
|
231
|
+
def select_server(server_arg)
|
232
|
+
case server_arg
|
233
|
+
when nil
|
234
|
+
raise MCPClient::Errors::ServerNotFound, 'No server available' if @servers.empty?
|
235
|
+
|
236
|
+
@servers.first
|
237
|
+
when Integer
|
238
|
+
@servers.fetch(server_arg) do
|
239
|
+
raise MCPClient::Errors::ServerNotFound, "Server at index #{server_arg} not found"
|
240
|
+
end
|
241
|
+
when String, Symbol
|
242
|
+
key = server_arg.to_s.downcase
|
243
|
+
srv = @servers.find { |s| s.class.name.split('::').last.downcase.end_with?(key) }
|
244
|
+
raise MCPClient::Errors::ServerNotFound, "Server of type #{server_arg} not found" unless srv
|
245
|
+
|
246
|
+
srv
|
247
|
+
else
|
248
|
+
raise ArgumentError, "Invalid server argument: #{server_arg.inspect}" unless @servers.include?(server_arg)
|
249
|
+
|
250
|
+
server_arg
|
251
|
+
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
190
255
|
# Validate parameters against tool JSON schema (checks required properties)
|
191
256
|
# @param tool [MCPClient::Tool] tool definition with schema
|
192
257
|
# @param parameters [Hash] parameters to validate
|
@@ -45,11 +45,10 @@ module MCPClient
|
|
45
45
|
raise NotImplementedError, 'Subclasses must implement rpc_notify'
|
46
46
|
end
|
47
47
|
|
48
|
-
# Ping the MCP server to check connectivity
|
49
|
-
# @param params [Hash] optional parameters for the ping request
|
48
|
+
# Ping the MCP server to check connectivity (zero-parameter heartbeat call)
|
50
49
|
# @return [Object] result from the ping request
|
51
|
-
def ping
|
52
|
-
rpc_request('ping'
|
50
|
+
def ping
|
51
|
+
rpc_request('ping')
|
53
52
|
end
|
54
53
|
|
55
54
|
# Register a callback to receive JSON-RPC notifications
|
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'uri'
|
4
|
-
require 'net/http'
|
5
4
|
require 'json'
|
6
|
-
require 'openssl'
|
7
5
|
require 'monitor'
|
8
6
|
require 'logger'
|
7
|
+
require 'faraday'
|
8
|
+
require 'faraday/retry'
|
9
9
|
|
10
10
|
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
|
-
attr_reader :base_url, :tools, :
|
14
|
+
attr_reader :base_url, :tools, :server_info, :capabilities
|
15
15
|
|
16
16
|
# @param base_url [String] The base URL of the MCP server
|
17
17
|
# @param headers [Hash] Additional headers to include in requests
|
@@ -33,10 +33,12 @@ module MCPClient
|
|
33
33
|
'Cache-Control' => 'no-cache',
|
34
34
|
'Connection' => 'keep-alive'
|
35
35
|
})
|
36
|
-
|
36
|
+
# HTTP client is managed via Faraday
|
37
37
|
@tools = nil
|
38
38
|
@read_timeout = read_timeout
|
39
|
-
|
39
|
+
|
40
|
+
# SSE-provided JSON-RPC endpoint path for POST requests
|
41
|
+
@rpc_endpoint = nil
|
40
42
|
@tools_data = nil
|
41
43
|
@request_id = 0
|
42
44
|
@sse_results = {}
|
@@ -132,19 +134,7 @@ module MCPClient
|
|
132
134
|
@mutex.synchronize do
|
133
135
|
return true if @connection_established
|
134
136
|
|
135
|
-
|
136
|
-
@http_client = Net::HTTP.new(uri.host, uri.port)
|
137
|
-
|
138
|
-
if uri.scheme == 'https'
|
139
|
-
@http_client.use_ssl = true
|
140
|
-
@http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
141
|
-
end
|
142
|
-
|
143
|
-
@http_client.open_timeout = 10
|
144
|
-
@http_client.read_timeout = @read_timeout
|
145
|
-
@http_client.keep_alive_timeout = 60
|
146
|
-
|
147
|
-
@http_client.start
|
137
|
+
# Start SSE listener using Faraday HTTP client
|
148
138
|
start_sse_thread
|
149
139
|
|
150
140
|
timeout = 10
|
@@ -179,7 +169,6 @@ module MCPClient
|
|
179
169
|
end
|
180
170
|
|
181
171
|
@tools = nil
|
182
|
-
@session_id = nil
|
183
172
|
@connection_established = false
|
184
173
|
@sse_connected = false
|
185
174
|
end
|
@@ -204,31 +193,31 @@ module MCPClient
|
|
204
193
|
# @return [void]
|
205
194
|
def rpc_notify(method, params = {})
|
206
195
|
ensure_initialized
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
196
|
+
uri = URI.parse(@base_url)
|
197
|
+
base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
198
|
+
rpc_ep = @mutex.synchronize { @rpc_endpoint }
|
199
|
+
@rpc_conn ||= Faraday.new(url: base) do |f|
|
200
|
+
f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
|
201
|
+
f.options.open_timeout = @read_timeout
|
202
|
+
f.options.timeout = @read_timeout
|
203
|
+
f.adapter Faraday.default_adapter
|
213
204
|
end
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
headers.except('Accept', 'Cache-Control').each { |k, v| http_req[k] = v }
|
223
|
-
response = http.request(http_req)
|
224
|
-
unless response.is_a?(Net::HTTPSuccess)
|
225
|
-
raise MCPClient::Errors::ServerError, "Notification failed: #{response.code} #{response.message}"
|
205
|
+
response = @rpc_conn.post(rpc_ep) do |req|
|
206
|
+
req.headers['Content-Type'] = 'application/json'
|
207
|
+
req.headers['Accept'] = 'application/json'
|
208
|
+
(@headers.dup.tap do |h|
|
209
|
+
h.delete('Accept')
|
210
|
+
h.delete('Cache-Control')
|
211
|
+
end).each do |k, v|
|
212
|
+
req.headers[k] = v
|
226
213
|
end
|
214
|
+
req.body = { jsonrpc: '2.0', method: method, params: params }.to_json
|
215
|
+
end
|
216
|
+
unless response.success?
|
217
|
+
raise MCPClient::Errors::ServerError, "Notification failed: #{response.status} #{response.reason_phrase}"
|
227
218
|
end
|
228
219
|
rescue StandardError => e
|
229
220
|
raise MCPClient::Errors::TransportError, "Failed to send notification: #{e.message}"
|
230
|
-
ensure
|
231
|
-
rpc_http.finish if rpc_http&.started?
|
232
221
|
end
|
233
222
|
|
234
223
|
private
|
@@ -269,60 +258,31 @@ module MCPClient
|
|
269
258
|
return if @sse_thread&.alive?
|
270
259
|
|
271
260
|
@sse_thread = Thread.new do
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
sse_http = Net::HTTP.new(uri.host, uri.port)
|
276
|
-
|
277
|
-
if uri.scheme == 'https'
|
278
|
-
sse_http.use_ssl = true
|
279
|
-
sse_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
280
|
-
end
|
281
|
-
|
282
|
-
sse_http.open_timeout = 10
|
283
|
-
sse_http.read_timeout = @read_timeout
|
284
|
-
sse_http.keep_alive_timeout = 60
|
285
|
-
|
286
|
-
sse_http.start do |http|
|
287
|
-
request = Net::HTTP::Get.new(uri)
|
288
|
-
@headers.each { |k, v| request[k] = v }
|
289
|
-
|
290
|
-
http.request(request) do |response|
|
291
|
-
unless response.is_a?(Net::HTTPSuccess) && response['content-type']&.start_with?('text/event-stream')
|
292
|
-
@mutex.synchronize do
|
293
|
-
# Signal connection attempt completed (failed)
|
294
|
-
@connection_established = false
|
295
|
-
@connection_cv.broadcast
|
296
|
-
end
|
297
|
-
raise MCPClient::Errors::ServerError, 'Server response not OK or not text/event-stream'
|
298
|
-
end
|
261
|
+
uri = URI.parse(@base_url)
|
262
|
+
sse_base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
263
|
+
sse_path = uri.request_uri
|
299
264
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
end
|
265
|
+
@sse_conn ||= Faraday.new(url: sse_base) do |f|
|
266
|
+
f.options.open_timeout = 10
|
267
|
+
f.options.timeout = nil
|
268
|
+
f.adapter Faraday.default_adapter
|
269
|
+
end
|
306
270
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
end
|
313
|
-
rescue StandardError
|
314
|
-
# On any SSE thread error, signal connection as established to unblock connect
|
315
|
-
@mutex.synchronize do
|
316
|
-
@connection_established = true
|
317
|
-
@connection_cv.broadcast
|
318
|
-
end
|
319
|
-
nil
|
320
|
-
ensure
|
321
|
-
sse_http&.finish if sse_http&.started?
|
322
|
-
@mutex.synchronize do
|
323
|
-
@sse_connected = false
|
271
|
+
@sse_conn.get(sse_path) do |req|
|
272
|
+
@headers.each { |k, v| req.headers[k] = v }
|
273
|
+
req.options.on_data = proc do |chunk, _bytes|
|
274
|
+
@logger.debug("SSE chunk received: #{chunk.inspect}")
|
275
|
+
process_sse_chunk(chunk.dup)
|
324
276
|
end
|
325
277
|
end
|
278
|
+
rescue StandardError
|
279
|
+
# On any SSE thread error, signal connection established to unblock connect
|
280
|
+
@mutex.synchronize do
|
281
|
+
@connection_established = true
|
282
|
+
@connection_cv.broadcast
|
283
|
+
end
|
284
|
+
ensure
|
285
|
+
@mutex.synchronize { @sse_connected = false }
|
326
286
|
end
|
327
287
|
end
|
328
288
|
|
@@ -352,14 +312,11 @@ module MCPClient
|
|
352
312
|
|
353
313
|
case event[:event]
|
354
314
|
when 'endpoint'
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
@
|
359
|
-
|
360
|
-
@connection_established = true
|
361
|
-
@connection_cv.broadcast
|
362
|
-
end
|
315
|
+
ep = event[:data]
|
316
|
+
@mutex.synchronize do
|
317
|
+
@rpc_endpoint = ep
|
318
|
+
@connection_established = true
|
319
|
+
@connection_cv.broadcast
|
363
320
|
end
|
364
321
|
when 'message'
|
365
322
|
begin
|
@@ -475,72 +432,56 @@ module MCPClient
|
|
475
432
|
def send_jsonrpc_request(request)
|
476
433
|
@logger.debug("Sending JSON-RPC request: #{request.to_json}")
|
477
434
|
uri = URI.parse(@base_url)
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
435
|
+
base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
436
|
+
rpc_ep = @mutex.synchronize { @rpc_endpoint }
|
437
|
+
|
438
|
+
@rpc_conn ||= Faraday.new(url: base) do |f|
|
439
|
+
f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
|
440
|
+
f.options.open_timeout = @read_timeout
|
441
|
+
f.options.timeout = @read_timeout
|
442
|
+
f.adapter Faraday.default_adapter
|
483
443
|
end
|
484
444
|
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
end
|
498
|
-
|
499
|
-
uri = URI.parse(url)
|
500
|
-
http_request = Net::HTTP::Post.new(uri)
|
501
|
-
http_request.content_type = 'application/json'
|
502
|
-
http_request.body = request.to_json
|
503
|
-
|
504
|
-
headers = @mutex.synchronize { @headers.dup }
|
505
|
-
headers.except('Accept', 'Cache-Control')
|
506
|
-
.each { |k, v| http_request[k] = v }
|
445
|
+
response = @rpc_conn.post(rpc_ep) do |req|
|
446
|
+
req.headers['Content-Type'] = 'application/json'
|
447
|
+
req.headers['Accept'] = 'application/json'
|
448
|
+
(@headers.dup.tap do |h|
|
449
|
+
h.delete('Accept')
|
450
|
+
h.delete('Cache-Control')
|
451
|
+
end).each do |k, v|
|
452
|
+
req.headers[k] = v
|
453
|
+
end
|
454
|
+
req.body = request.to_json
|
455
|
+
end
|
456
|
+
@logger.debug("Received JSON-RPC response: #{response.status} #{response.body}")
|
507
457
|
|
508
|
-
|
509
|
-
|
458
|
+
unless response.success?
|
459
|
+
raise MCPClient::Errors::ServerError, "Server returned error: #{response.status} #{response.reason_phrase}"
|
460
|
+
end
|
510
461
|
|
511
|
-
|
512
|
-
|
462
|
+
if @use_sse
|
463
|
+
# Wait for result via SSE channel
|
464
|
+
request_id = request[:id]
|
465
|
+
start_time = Time.now
|
466
|
+
timeout = @read_timeout || 10
|
467
|
+
loop do
|
468
|
+
result = nil
|
469
|
+
@mutex.synchronize do
|
470
|
+
result = @sse_results.delete(request_id) if @sse_results.key?(request_id)
|
513
471
|
end
|
472
|
+
return result if result
|
473
|
+
break if Time.now - start_time > timeout
|
514
474
|
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
result = @sse_results.delete(request_id) if @sse_results.key?(request_id)
|
525
|
-
end
|
526
|
-
break if result || (Time.now - start_time > timeout)
|
527
|
-
|
528
|
-
sleep 0.1
|
529
|
-
end
|
530
|
-
return result if result
|
531
|
-
|
532
|
-
raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
|
533
|
-
end
|
534
|
-
# Fallback: parse synchronous HTTP JSON response
|
535
|
-
begin
|
536
|
-
data = JSON.parse(response.body)
|
537
|
-
return data['result']
|
538
|
-
rescue JSON::ParserError => e
|
539
|
-
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
|
540
|
-
end
|
475
|
+
sleep 0.1
|
476
|
+
end
|
477
|
+
raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
|
478
|
+
else
|
479
|
+
begin
|
480
|
+
data = JSON.parse(response.body)
|
481
|
+
data['result']
|
482
|
+
rescue JSON::ParserError => e
|
483
|
+
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
|
541
484
|
end
|
542
|
-
ensure
|
543
|
-
rpc_http.finish if rpc_http.started?
|
544
485
|
end
|
545
486
|
end
|
546
487
|
end
|
data/lib/mcp_client/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-mcp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.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-04-
|
11
|
+
date: 2025-04-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday-retry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: rdoc
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
126
|
requirements:
|
99
127
|
- - ">="
|
100
128
|
- !ruby/object:Gem::Version
|
101
|
-
version: 2.
|
129
|
+
version: 3.2.0
|
102
130
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
131
|
requirements:
|
104
132
|
- - ">="
|