ruby-mcp-client 1.0.1 → 1.1.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 +113 -4
- data/lib/mcp_client/auth/oauth_provider.rb +333 -40
- data/lib/mcp_client/auth.rb +11 -4
- data/lib/mcp_client/client.rb +197 -34
- data/lib/mcp_client/errors.rb +9 -0
- data/lib/mcp_client/http_transport_base.rb +5 -1
- data/lib/mcp_client/json_rpc_common.rb +19 -4
- data/lib/mcp_client/server_base.rb +73 -0
- data/lib/mcp_client/server_http.rb +27 -17
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +4 -1
- data/lib/mcp_client/server_sse/reconnect_monitor.rb +4 -0
- data/lib/mcp_client/server_sse/sse_parser.rb +5 -2
- data/lib/mcp_client/server_sse.rb +49 -36
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +13 -3
- data/lib/mcp_client/server_stdio.rb +124 -20
- data/lib/mcp_client/server_streamable_http.rb +11 -30
- data/lib/mcp_client/task.rb +93 -61
- data/lib/mcp_client/tool.rb +48 -10
- data/lib/mcp_client/version.rb +1 -1
- metadata +18 -4
|
@@ -405,28 +405,42 @@ module MCPClient
|
|
|
405
405
|
@sse_connected = false
|
|
406
406
|
@initialized = false # Reset initialization state for reconnection
|
|
407
407
|
|
|
408
|
+
# Reset the SSE parse buffer so a reconnect never inherits a leftover
|
|
409
|
+
# partial event from the previous connection.
|
|
410
|
+
@buffer = ''
|
|
411
|
+
|
|
408
412
|
# Log cleanup for debugging
|
|
409
413
|
@logger.debug('Cleaning up SSE connection')
|
|
410
414
|
|
|
411
415
|
# Store threads locally to avoid race conditions
|
|
412
416
|
sse_thread = @sse_thread
|
|
417
|
+
|
|
418
|
+
# The activity monitor drives reconnection, and reconnection calls this
|
|
419
|
+
# method FROM that thread. Killing the current thread here would abort
|
|
420
|
+
# the reconnect before connect() runs (the historical dead-code bug), so
|
|
421
|
+
# never kill/clear the activity thread when we are running on it. Leaving
|
|
422
|
+
# @activity_timer_thread referenced also makes start_activity_monitor
|
|
423
|
+
# short-circuit, preventing a duplicate monitor after reconnect.
|
|
413
424
|
activity_thread = @activity_timer_thread
|
|
425
|
+
kill_activity_thread = activity_thread && activity_thread != Thread.current
|
|
414
426
|
|
|
415
427
|
# Clear thread references first
|
|
416
428
|
@sse_thread = nil
|
|
417
|
-
@activity_timer_thread = nil
|
|
429
|
+
@activity_timer_thread = nil if kill_activity_thread
|
|
418
430
|
|
|
419
|
-
# Kill threads
|
|
431
|
+
# Kill threads
|
|
420
432
|
begin
|
|
421
433
|
sse_thread&.kill
|
|
422
434
|
rescue StandardError => e
|
|
423
435
|
@logger.debug("Error killing SSE thread: #{e.message}")
|
|
424
436
|
end
|
|
425
437
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
438
|
+
if kill_activity_thread
|
|
439
|
+
begin
|
|
440
|
+
activity_thread.kill
|
|
441
|
+
rescue StandardError => e
|
|
442
|
+
@logger.debug("Error killing activity thread: #{e.message}")
|
|
443
|
+
end
|
|
430
444
|
end
|
|
431
445
|
|
|
432
446
|
if @http_client
|
|
@@ -476,6 +490,8 @@ module MCPClient
|
|
|
476
490
|
@logger.debug("Received server request: #{method} (id: #{request_id})")
|
|
477
491
|
|
|
478
492
|
case method
|
|
493
|
+
when 'ping'
|
|
494
|
+
handle_ping(request_id)
|
|
479
495
|
when 'elicitation/create'
|
|
480
496
|
handle_elicitation_create(request_id, params)
|
|
481
497
|
when 'roots/list'
|
|
@@ -491,6 +507,21 @@ module MCPClient
|
|
|
491
507
|
send_error_response(request_id, -32_603, "Internal error: #{e.message}")
|
|
492
508
|
end
|
|
493
509
|
|
|
510
|
+
# Handle a server-initiated ping request (MCP ping utility)
|
|
511
|
+
# The receiver MUST respond promptly with an empty result.
|
|
512
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
513
|
+
# @return [void]
|
|
514
|
+
def handle_ping(request_id)
|
|
515
|
+
response = {
|
|
516
|
+
'jsonrpc' => '2.0',
|
|
517
|
+
'id' => request_id,
|
|
518
|
+
'result' => {}
|
|
519
|
+
}
|
|
520
|
+
post_jsonrpc_response(response)
|
|
521
|
+
rescue StandardError => e
|
|
522
|
+
@logger.error("Error sending ping response: #{e.message}")
|
|
523
|
+
end
|
|
524
|
+
|
|
494
525
|
# Handle elicitation/create request from server (MCP 2025-06-18)
|
|
495
526
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
496
527
|
# @param params [Hash] the elicitation parameters
|
|
@@ -889,24 +920,14 @@ module MCPClient
|
|
|
889
920
|
# @private
|
|
890
921
|
def request_prompts_list
|
|
891
922
|
@mutex.synchronize do
|
|
892
|
-
return @prompts_data if @prompts_data
|
|
923
|
+
return @prompts_data.dup if @prompts_data
|
|
893
924
|
end
|
|
894
925
|
|
|
895
|
-
|
|
926
|
+
# Follow nextCursor across pages so the full prompt list is returned.
|
|
927
|
+
prompts = request_paginated_list('prompts/list', 'prompts')
|
|
896
928
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
@prompts_data = result['prompts']
|
|
900
|
-
end
|
|
901
|
-
return @mutex.synchronize { @prompts_data.dup }
|
|
902
|
-
elsif result
|
|
903
|
-
@mutex.synchronize do
|
|
904
|
-
@prompts_data = result
|
|
905
|
-
end
|
|
906
|
-
return @mutex.synchronize { @prompts_data.dup }
|
|
907
|
-
end
|
|
908
|
-
|
|
909
|
-
raise MCPClient::Errors::PromptGetError, 'Failed to get prompts list from JSON-RPC request'
|
|
929
|
+
@mutex.synchronize { @prompts_data = prompts }
|
|
930
|
+
@mutex.synchronize { @prompts_data.dup }
|
|
910
931
|
end
|
|
911
932
|
|
|
912
933
|
# Request the resources list using JSON-RPC
|
|
@@ -941,24 +962,16 @@ module MCPClient
|
|
|
941
962
|
# @private
|
|
942
963
|
def request_tools_list
|
|
943
964
|
@mutex.synchronize do
|
|
944
|
-
return @tools_data if @tools_data
|
|
965
|
+
return @tools_data.dup if @tools_data
|
|
945
966
|
end
|
|
946
967
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
@tools_data = result['tools']
|
|
952
|
-
end
|
|
953
|
-
return @mutex.synchronize { @tools_data.dup }
|
|
954
|
-
elsif result
|
|
955
|
-
@mutex.synchronize do
|
|
956
|
-
@tools_data = result
|
|
957
|
-
end
|
|
958
|
-
return @mutex.synchronize { @tools_data.dup }
|
|
959
|
-
end
|
|
968
|
+
# Follow nextCursor across pages so the full tool list is returned. The
|
|
969
|
+
# SSE parser no longer writes @tools_data per page, so this method is the
|
|
970
|
+
# sole writer and only ever caches the COMPLETE list.
|
|
971
|
+
tools = request_paginated_list('tools/list', 'tools')
|
|
960
972
|
|
|
961
|
-
|
|
973
|
+
@mutex.synchronize { @tools_data = tools }
|
|
974
|
+
@mutex.synchronize { @tools_data.dup }
|
|
962
975
|
end
|
|
963
976
|
end
|
|
964
977
|
end
|
|
@@ -16,6 +16,7 @@ module MCPClient
|
|
|
16
16
|
|
|
17
17
|
connect
|
|
18
18
|
start_reader
|
|
19
|
+
start_stderr_reader
|
|
19
20
|
perform_initialize
|
|
20
21
|
|
|
21
22
|
@initialized = true
|
|
@@ -44,12 +45,15 @@ module MCPClient
|
|
|
44
45
|
@stdin.puts(notif.to_json)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
# Generate a new unique request ID
|
|
48
|
+
# Generate a new unique request ID and mark it as awaiting a response.
|
|
49
|
+
# Registering the id before the request is sent lets the reader thread
|
|
50
|
+
# distinguish expected responses from late/unsolicited ones.
|
|
48
51
|
# @return [Integer] a unique request ID
|
|
49
52
|
def next_id
|
|
50
53
|
@mutex.synchronize do
|
|
51
54
|
id = @next_id
|
|
52
55
|
@next_id += 1
|
|
56
|
+
@awaiting[id] = true
|
|
53
57
|
id
|
|
54
58
|
end
|
|
55
59
|
end
|
|
@@ -62,6 +66,10 @@ module MCPClient
|
|
|
62
66
|
@logger.debug("Sending JSONRPC request: #{req.to_json}")
|
|
63
67
|
@stdin.puts(req.to_json)
|
|
64
68
|
rescue StandardError => e
|
|
69
|
+
# A request that failed to send will never receive a response, so drop
|
|
70
|
+
# its awaiting marker; otherwise a broken transport (e.g. the server
|
|
71
|
+
# exited) would leak an entry per retry/attempt into @awaiting.
|
|
72
|
+
@mutex.synchronize { @awaiting.delete(req['id']) } if req.is_a?(Hash) && req['id']
|
|
65
73
|
raise MCPClient::Errors::TransportError, "Failed to send JSONRPC request: #{e.message}"
|
|
66
74
|
end
|
|
67
75
|
|
|
@@ -78,8 +86,10 @@ module MCPClient
|
|
|
78
86
|
|
|
79
87
|
@cond.wait(@mutex, remaining)
|
|
80
88
|
end
|
|
81
|
-
|
|
82
|
-
@pending
|
|
89
|
+
# Remove the response and the awaiting marker on both success and
|
|
90
|
+
# timeout so neither @pending nor @awaiting accumulates entries.
|
|
91
|
+
msg = @pending.delete(id)
|
|
92
|
+
@awaiting.delete(id)
|
|
83
93
|
raise MCPClient::Errors::TransportError, "Timeout waiting for JSONRPC response id=#{id}" unless msg
|
|
84
94
|
|
|
85
95
|
msg
|
|
@@ -21,6 +21,14 @@ module MCPClient
|
|
|
21
21
|
# Timeout in seconds for responses
|
|
22
22
|
READ_TIMEOUT = 15
|
|
23
23
|
|
|
24
|
+
# Chunk size (bytes) used when draining the subprocess stderr pipe
|
|
25
|
+
STDERR_READ_CHUNK_SIZE = 8192
|
|
26
|
+
|
|
27
|
+
# Maximum bytes buffered for a single unterminated stderr line before it is
|
|
28
|
+
# flushed. Bounds memory when a server writes to stderr without newlines
|
|
29
|
+
# (e.g. progress output using carriage returns).
|
|
30
|
+
STDERR_MAX_LINE_SIZE = 64 * 1024
|
|
31
|
+
|
|
24
32
|
# Initialize a new ServerStdio instance
|
|
25
33
|
# @param command [String, Array] the stdio command to launch the MCP JSON-RPC server
|
|
26
34
|
# For improved security, passing an Array is recommended to avoid shell injection issues
|
|
@@ -38,6 +46,9 @@ module MCPClient
|
|
|
38
46
|
@cond = ConditionVariable.new
|
|
39
47
|
@next_id = 1
|
|
40
48
|
@pending = {}
|
|
49
|
+
# Ids of requests awaiting a response; used to drop late/unsolicited
|
|
50
|
+
# responses so @pending cannot grow without bound on a long-lived session
|
|
51
|
+
@awaiting = {}
|
|
41
52
|
@initialized = false
|
|
42
53
|
@server_info = nil
|
|
43
54
|
@capabilities = nil
|
|
@@ -49,6 +60,8 @@ module MCPClient
|
|
|
49
60
|
@elicitation_request_callback = nil # MCP 2025-06-18
|
|
50
61
|
@roots_list_request_callback = nil # MCP 2025-06-18
|
|
51
62
|
@sampling_request_callback = nil # MCP 2025-11-25
|
|
63
|
+
@reader_thread = nil
|
|
64
|
+
@stderr_thread = nil
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
# Server info from the initialize response
|
|
@@ -91,6 +104,36 @@ module MCPClient
|
|
|
91
104
|
end
|
|
92
105
|
end
|
|
93
106
|
|
|
107
|
+
# Spawn a thread to continuously drain the subprocess stderr.
|
|
108
|
+
#
|
|
109
|
+
# The child's stderr pipe has a fixed OS buffer (typically 64KB). If it is
|
|
110
|
+
# never read, a server that logs verbosely to stderr eventually blocks on
|
|
111
|
+
# write once the buffer fills, which stalls the whole subprocess (it stops
|
|
112
|
+
# producing stdout / reading stdin) and deadlocks the client. Draining
|
|
113
|
+
# stderr keeps the pipe empty; lines are surfaced at debug level.
|
|
114
|
+
#
|
|
115
|
+
# Reads happen in bounded chunks (not IO#each_line) so that a server which
|
|
116
|
+
# writes to stderr without newline delimiters cannot make the client buffer
|
|
117
|
+
# a single "line" without limit: any pending fragment larger than
|
|
118
|
+
# STDERR_MAX_LINE_SIZE is flushed rather than retained.
|
|
119
|
+
# @return [Thread] the stderr reader thread
|
|
120
|
+
def start_stderr_reader
|
|
121
|
+
@stderr_thread = Thread.new do
|
|
122
|
+
buffer = +''
|
|
123
|
+
loop do
|
|
124
|
+
buffer << @stderr.readpartial(STDERR_READ_CHUNK_SIZE)
|
|
125
|
+
flush_stderr_lines(buffer)
|
|
126
|
+
flush_stderr_overflow(buffer)
|
|
127
|
+
end
|
|
128
|
+
rescue IOError
|
|
129
|
+
# EOFError (a subclass of IOError) on EOF, or IOError on close;
|
|
130
|
+
# emit any trailing partial line before exiting
|
|
131
|
+
@logger.debug("[stderr] #{buffer.chomp}") if buffer && !buffer.empty?
|
|
132
|
+
rescue StandardError
|
|
133
|
+
# reader aborted unexpectedly; nothing actionable
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
94
137
|
# Handle a line of output from the stdio server
|
|
95
138
|
# Parses JSON-RPC messages and adds them to pending responses
|
|
96
139
|
# @param line [String] line of output to parse
|
|
@@ -116,8 +159,15 @@ module MCPClient
|
|
|
116
159
|
return unless id
|
|
117
160
|
|
|
118
161
|
@mutex.synchronize do
|
|
119
|
-
|
|
120
|
-
|
|
162
|
+
# Only retain a response that corresponds to an outstanding request.
|
|
163
|
+
# Late responses (arriving after the caller timed out) and unsolicited
|
|
164
|
+
# responses are dropped so @pending cannot grow without bound.
|
|
165
|
+
if @awaiting.key?(id)
|
|
166
|
+
@pending[id] = msg
|
|
167
|
+
@cond.broadcast
|
|
168
|
+
else
|
|
169
|
+
@logger.debug("Discarding response for unknown or expired request id=#{id}")
|
|
170
|
+
end
|
|
121
171
|
end
|
|
122
172
|
rescue JSON::ParserError
|
|
123
173
|
# Skip non-JSONRPC lines in the output stream
|
|
@@ -129,15 +179,21 @@ module MCPClient
|
|
|
129
179
|
# @raise [MCPClient::Errors::PromptGetError] for other errors during prompt listing
|
|
130
180
|
def list_prompts
|
|
131
181
|
ensure_initialized
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
182
|
+
collect_paginated('prompts') do |cursor|
|
|
183
|
+
params = {}
|
|
184
|
+
params['cursor'] = cursor if cursor
|
|
185
|
+
req_id = next_id
|
|
186
|
+
req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'prompts/list', 'params' => params }
|
|
187
|
+
send_request(req)
|
|
188
|
+
res = wait_response(req_id)
|
|
189
|
+
if (err = res['error'])
|
|
190
|
+
raise MCPClient::Errors::ServerError, err['message']
|
|
191
|
+
end
|
|
139
192
|
|
|
140
|
-
|
|
193
|
+
result = res['result'] || {}
|
|
194
|
+
prompts = (result['prompts'] || []).map { |td| MCPClient::Prompt.from_json(td, server: self) }
|
|
195
|
+
[prompts, result['nextCursor']]
|
|
196
|
+
end
|
|
141
197
|
rescue StandardError => e
|
|
142
198
|
raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
|
|
143
199
|
end
|
|
@@ -301,16 +357,22 @@ module MCPClient
|
|
|
301
357
|
# @raise [MCPClient::Errors::ToolCallError] for other errors during tool listing
|
|
302
358
|
def list_tools
|
|
303
359
|
ensure_initialized
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
360
|
+
collect_paginated('tools') do |cursor|
|
|
361
|
+
params = {}
|
|
362
|
+
params['cursor'] = cursor if cursor
|
|
363
|
+
req_id = next_id
|
|
364
|
+
# JSON-RPC method for listing tools
|
|
365
|
+
req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'tools/list', 'params' => params }
|
|
366
|
+
send_request(req)
|
|
367
|
+
res = wait_response(req_id)
|
|
368
|
+
if (err = res['error'])
|
|
369
|
+
raise MCPClient::Errors::ServerError, err['message']
|
|
370
|
+
end
|
|
312
371
|
|
|
313
|
-
|
|
372
|
+
result = res['result'] || {}
|
|
373
|
+
tools = (result['tools'] || []).map { |td| MCPClient::Tool.from_json(td, server: self) }
|
|
374
|
+
[tools, result['nextCursor']]
|
|
375
|
+
end
|
|
314
376
|
rescue StandardError => e
|
|
315
377
|
raise MCPClient::Errors::ToolCallError, "Error listing tools: #{e.message}"
|
|
316
378
|
end
|
|
@@ -427,6 +489,8 @@ module MCPClient
|
|
|
427
489
|
@logger.debug("Received server request: #{method} (id: #{request_id})")
|
|
428
490
|
|
|
429
491
|
case method
|
|
492
|
+
when 'ping'
|
|
493
|
+
handle_ping(request_id)
|
|
430
494
|
when 'elicitation/create'
|
|
431
495
|
handle_elicitation_create(request_id, params)
|
|
432
496
|
when 'roots/list'
|
|
@@ -442,6 +506,19 @@ module MCPClient
|
|
|
442
506
|
send_error_response(request_id, -32_603, "Internal error: #{e.message}")
|
|
443
507
|
end
|
|
444
508
|
|
|
509
|
+
# Handle a server-initiated ping request (MCP ping utility)
|
|
510
|
+
# The receiver MUST respond promptly with an empty result.
|
|
511
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
512
|
+
# @return [void]
|
|
513
|
+
def handle_ping(request_id)
|
|
514
|
+
response = {
|
|
515
|
+
'jsonrpc' => '2.0',
|
|
516
|
+
'id' => request_id,
|
|
517
|
+
'result' => {}
|
|
518
|
+
}
|
|
519
|
+
send_message(response)
|
|
520
|
+
end
|
|
521
|
+
|
|
445
522
|
# Handle elicitation/create request from server (MCP 2025-06-18)
|
|
446
523
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
447
524
|
# @param params [Hash] the elicitation parameters
|
|
@@ -573,6 +650,27 @@ module MCPClient
|
|
|
573
650
|
@logger.error("Error sending message: #{e.message}")
|
|
574
651
|
end
|
|
575
652
|
|
|
653
|
+
# Emit and remove all newline-terminated lines from the stderr buffer.
|
|
654
|
+
# @param buffer [String] mutable buffer of accumulated stderr bytes
|
|
655
|
+
# @return [void]
|
|
656
|
+
def flush_stderr_lines(buffer)
|
|
657
|
+
while (newline_index = buffer.index("\n"))
|
|
658
|
+
line = buffer.slice!(0, newline_index + 1)
|
|
659
|
+
@logger.debug("[stderr] #{line.chomp}")
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Flush an unterminated stderr fragment that has grown past the size cap,
|
|
664
|
+
# so a newline-less stderr stream cannot buffer without bound.
|
|
665
|
+
# @param buffer [String] mutable buffer of accumulated stderr bytes
|
|
666
|
+
# @return [void]
|
|
667
|
+
def flush_stderr_overflow(buffer)
|
|
668
|
+
return if buffer.bytesize <= STDERR_MAX_LINE_SIZE
|
|
669
|
+
|
|
670
|
+
@logger.debug("[stderr] #{buffer}")
|
|
671
|
+
buffer.clear
|
|
672
|
+
end
|
|
673
|
+
|
|
576
674
|
# Clean up the server connection
|
|
577
675
|
# Closes all stdio handles and terminates any running processes and threads
|
|
578
676
|
# @return [void]
|
|
@@ -587,10 +685,16 @@ module MCPClient
|
|
|
587
685
|
@wait_thread.join(1)
|
|
588
686
|
end
|
|
589
687
|
@reader_thread&.kill
|
|
688
|
+
@stderr_thread&.kill
|
|
590
689
|
rescue StandardError
|
|
591
690
|
# Clean up resources during unexpected termination
|
|
592
691
|
ensure
|
|
593
|
-
|
|
692
|
+
# Release any buffered responses / awaiting markers
|
|
693
|
+
@mutex.synchronize do
|
|
694
|
+
@pending.clear
|
|
695
|
+
@awaiting.clear
|
|
696
|
+
end
|
|
697
|
+
@stdin = @stdout = @stderr = @wait_thread = @reader_thread = @stderr_thread = nil
|
|
594
698
|
end
|
|
595
699
|
end
|
|
596
700
|
end
|
|
@@ -575,24 +575,15 @@ module MCPClient
|
|
|
575
575
|
# @raise [MCPClient::Errors::ToolCallError] if tools list retrieval fails
|
|
576
576
|
def request_tools_list
|
|
577
577
|
@mutex.synchronize do
|
|
578
|
-
return @tools_data if @tools_data
|
|
578
|
+
return @tools_data.dup if @tools_data
|
|
579
579
|
end
|
|
580
580
|
|
|
581
|
-
|
|
581
|
+
# Follow nextCursor across pages so the full tool list is returned even
|
|
582
|
+
# when the server paginates.
|
|
583
|
+
tools = request_paginated_list('tools/list', 'tools')
|
|
582
584
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
@tools_data = result['tools']
|
|
586
|
-
end
|
|
587
|
-
return @mutex.synchronize { @tools_data.dup }
|
|
588
|
-
elsif result.is_a?(Array) || result
|
|
589
|
-
@mutex.synchronize do
|
|
590
|
-
@tools_data = result
|
|
591
|
-
end
|
|
592
|
-
return @mutex.synchronize { @tools_data.dup }
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
raise MCPClient::Errors::ToolCallError, 'Failed to get tools list from JSON-RPC request'
|
|
585
|
+
@mutex.synchronize { @tools_data = tools }
|
|
586
|
+
@mutex.synchronize { @tools_data.dup }
|
|
596
587
|
end
|
|
597
588
|
|
|
598
589
|
# Request the prompts list using JSON-RPC
|
|
@@ -600,24 +591,14 @@ module MCPClient
|
|
|
600
591
|
# @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
|
|
601
592
|
def request_prompts_list
|
|
602
593
|
@mutex.synchronize do
|
|
603
|
-
return @prompts_data if @prompts_data
|
|
594
|
+
return @prompts_data.dup if @prompts_data
|
|
604
595
|
end
|
|
605
596
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if result.is_a?(Hash) && result['prompts']
|
|
609
|
-
@mutex.synchronize do
|
|
610
|
-
@prompts_data = result['prompts']
|
|
611
|
-
end
|
|
612
|
-
return @mutex.synchronize { @prompts_data.dup }
|
|
613
|
-
elsif result.is_a?(Array) || result
|
|
614
|
-
@mutex.synchronize do
|
|
615
|
-
@prompts_data = result
|
|
616
|
-
end
|
|
617
|
-
return @mutex.synchronize { @prompts_data.dup }
|
|
618
|
-
end
|
|
597
|
+
# Follow nextCursor across pages so the full prompt list is returned.
|
|
598
|
+
prompts = request_paginated_list('prompts/list', 'prompts')
|
|
619
599
|
|
|
620
|
-
|
|
600
|
+
@mutex.synchronize { @prompts_data = prompts }
|
|
601
|
+
@mutex.synchronize { @prompts_data.dup }
|
|
621
602
|
end
|
|
622
603
|
|
|
623
604
|
# Request the resources list using JSON-RPC
|