ruby-mcp-client 0.7.3 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,9 +17,11 @@ module MCPClient
17
17
 
18
18
  include SseParser
19
19
  include JsonRpcTransport
20
+
20
21
  require_relative 'server_sse/reconnect_monitor'
21
22
 
22
23
  include ReconnectMonitor
24
+
23
25
  # Ratio of close_after timeout to ping interval
24
26
  CLOSE_AFTER_PING_RATIO = 2.5
25
27
 
@@ -36,7 +38,11 @@ module MCPClient
36
38
  # @return [String] The base URL of the MCP server
37
39
  # @!attribute [r] tools
38
40
  # @return [Array<MCPClient::Tool>, nil] List of available tools (nil if not fetched yet)
39
- attr_reader :base_url, :tools
41
+ # @!attribute [r] prompts
42
+ # @return [Array<MCPClient::Prompt>, nil] List of available prompts (nil if not fetched yet)
43
+ # @!attribute [r] resources
44
+ # @return [Array<MCPClient::Resource>, nil] List of available resources (nil if not fetched yet)
45
+ attr_reader :base_url, :tools, :prompts, :resources
40
46
 
41
47
  # Server information from initialize response
42
48
  # @return [Hash, nil] Server information
@@ -104,6 +110,164 @@ module MCPClient
104
110
  end
105
111
  end
106
112
 
113
+ # List all prompts available from the MCP server
114
+ # @return [Array<MCPClient::Prompt>] list of available prompts
115
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
116
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
117
+ # @raise [MCPClient::Errors::PromptGetError] for other errors during prompt listing
118
+ def list_prompts
119
+ @mutex.synchronize do
120
+ return @prompts if @prompts
121
+ end
122
+
123
+ begin
124
+ ensure_initialized
125
+
126
+ prompts_data = request_prompts_list
127
+ @mutex.synchronize do
128
+ @prompts = prompts_data.map do |prompt_data|
129
+ MCPClient::Prompt.from_json(prompt_data, server: self)
130
+ end
131
+ end
132
+
133
+ @mutex.synchronize { @prompts }
134
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
135
+ # Re-raise these errors directly
136
+ raise
137
+ rescue StandardError => e
138
+ raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
139
+ end
140
+ end
141
+
142
+ # Get a prompt with the given parameters
143
+ # @param prompt_name [String] the name of the prompt to get
144
+ # @param parameters [Hash] the parameters to pass to the prompt
145
+ # @return [Object] the result of the prompt interpolation
146
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
147
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
148
+ # @raise [MCPClient::Errors::PromptGetError] for other errors during prompt interpolation
149
+ # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
150
+ def get_prompt(prompt_name, parameters)
151
+ rpc_request('prompts/get', {
152
+ name: prompt_name,
153
+ arguments: parameters
154
+ })
155
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
156
+ # Re-raise connection/transport errors directly to match test expectations
157
+ raise
158
+ rescue StandardError => e
159
+ # For all other errors, wrap in PromptGetError
160
+ raise MCPClient::Errors::PromptGetError, "Error get prompt '#{prompt_name}': #{e.message}"
161
+ end
162
+
163
+ # List all resources available from the MCP server
164
+ # @param cursor [String, nil] optional cursor for pagination
165
+ # @return [Hash] result containing resources array and optional nextCursor
166
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
167
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
168
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
169
+ def list_resources(cursor: nil)
170
+ @mutex.synchronize do
171
+ return @resources_result if @resources_result && !cursor
172
+ end
173
+
174
+ begin
175
+ ensure_initialized
176
+
177
+ params = {}
178
+ params['cursor'] = cursor if cursor
179
+ result = rpc_request('resources/list', params)
180
+
181
+ resources = (result['resources'] || []).map do |resource_data|
182
+ MCPClient::Resource.from_json(resource_data, server: self)
183
+ end
184
+
185
+ resources_result = { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
186
+
187
+ @mutex.synchronize do
188
+ @resources_result = resources_result unless cursor
189
+ end
190
+
191
+ resources_result
192
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
193
+ # Re-raise these errors directly
194
+ raise
195
+ rescue StandardError => e
196
+ raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
197
+ end
198
+ end
199
+
200
+ # Read a resource by its URI
201
+ # @param uri [String] the URI of the resource to read
202
+ # @return [Array<MCPClient::ResourceContent>] array of resource contents
203
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
204
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
205
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
206
+ # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
207
+ def read_resource(uri)
208
+ result = rpc_request('resources/read', { uri: uri })
209
+ contents = result['contents'] || []
210
+ contents.map { |content| MCPClient::ResourceContent.from_json(content) }
211
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
212
+ # Re-raise connection/transport errors directly to match test expectations
213
+ raise
214
+ rescue StandardError => e
215
+ # For all other errors, wrap in ResourceReadError
216
+ raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
217
+ end
218
+
219
+ # List all resource templates available from the MCP server
220
+ # @param cursor [String, nil] optional cursor for pagination
221
+ # @return [Hash] result containing resourceTemplates array and optional nextCursor
222
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
223
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
224
+ def list_resource_templates(cursor: nil)
225
+ ensure_initialized
226
+ params = {}
227
+ params['cursor'] = cursor if cursor
228
+ result = rpc_request('resources/templates/list', params)
229
+
230
+ templates = (result['resourceTemplates'] || []).map do |template_data|
231
+ MCPClient::ResourceTemplate.from_json(template_data, server: self)
232
+ end
233
+
234
+ { 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
235
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
236
+ raise
237
+ rescue StandardError => e
238
+ raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
239
+ end
240
+
241
+ # Subscribe to resource updates
242
+ # @param uri [String] the URI of the resource to subscribe to
243
+ # @return [Boolean] true if subscription successful
244
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
245
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
246
+ def subscribe_resource(uri)
247
+ ensure_initialized
248
+ rpc_request('resources/subscribe', { uri: uri })
249
+ true
250
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
251
+ raise
252
+ rescue StandardError => e
253
+ raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
254
+ end
255
+
256
+ # Unsubscribe from resource updates
257
+ # @param uri [String] the URI of the resource to unsubscribe from
258
+ # @return [Boolean] true if unsubscription successful
259
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
260
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
261
+ def unsubscribe_resource(uri)
262
+ ensure_initialized
263
+ rpc_request('resources/unsubscribe', { uri: uri })
264
+ true
265
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
266
+ raise
267
+ rescue StandardError => e
268
+ raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
269
+ end
270
+
107
271
  # List all tools available from the MCP server
108
272
  # @return [Array<MCPClient::Tool>] list of available tools
109
273
  # @raise [MCPClient::Errors::ServerError] if server returns an error
@@ -465,6 +629,58 @@ module MCPClient
465
629
  raise MCPClient::Errors::ConnectionError, "Authorization failed: #{error_message}"
466
630
  end
467
631
 
632
+ # Request the prompts list using JSON-RPC
633
+ # @return [Array<Hash>] the prompts data
634
+ # @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
635
+ # @private
636
+ def request_prompts_list
637
+ @mutex.synchronize do
638
+ return @prompts_data if @prompts_data
639
+ end
640
+
641
+ result = rpc_request('prompts/list')
642
+
643
+ if result && result['prompts']
644
+ @mutex.synchronize do
645
+ @prompts_data = result['prompts']
646
+ end
647
+ return @mutex.synchronize { @prompts_data.dup }
648
+ elsif result
649
+ @mutex.synchronize do
650
+ @prompts_data = result
651
+ end
652
+ return @mutex.synchronize { @prompts_data.dup }
653
+ end
654
+
655
+ raise MCPClient::Errors::PromptGetError, 'Failed to get prompts list from JSON-RPC request'
656
+ end
657
+
658
+ # Request the resources list using JSON-RPC
659
+ # @return [Array<Hash>] the resources data
660
+ # @raise [MCPClient::Errors::ResourceReadError] if resources list retrieval fails
661
+ # @private
662
+ def request_resources_list
663
+ @mutex.synchronize do
664
+ return @resources_data if @resources_data
665
+ end
666
+
667
+ result = rpc_request('resources/list')
668
+
669
+ if result && result['resources']
670
+ @mutex.synchronize do
671
+ @resources_data = result['resources']
672
+ end
673
+ return @mutex.synchronize { @resources_data.dup }
674
+ elsif result
675
+ @mutex.synchronize do
676
+ @resources_data = result
677
+ end
678
+ return @mutex.synchronize { @resources_data.dup }
679
+ end
680
+
681
+ raise MCPClient::Errors::ResourceReadError, 'Failed to get resources list from JSON-RPC request'
682
+ end
683
+
468
684
  # Request the tools list using JSON-RPC
469
685
  # @return [Array<Hash>] the tools data
470
686
  # @raise [MCPClient::Errors::ToolCallError] if tools list retrieval fails
@@ -7,6 +7,7 @@ module MCPClient
7
7
  # JSON-RPC request/notification plumbing for stdio transport
8
8
  module JsonRpcTransport
9
9
  include JsonRpcCommon
10
+
10
11
  # Ensure the server process is started and initialized (handshake)
11
12
  # @return [void]
12
13
  # @raise [MCPClient::Errors::ConnectionError] if initialization fails
@@ -33,6 +34,11 @@ module MCPClient
33
34
  raise MCPClient::Errors::ConnectionError, "Initialize failed: #{err['message']}"
34
35
  end
35
36
 
37
+ # Store server info and capabilities
38
+ result = res['result'] || {}
39
+ @server_info = result['serverInfo']
40
+ @capabilities = result['capabilities']
41
+
36
42
  # Send initialized notification
37
43
  notif = build_jsonrpc_notification('notifications/initialized', {})
38
44
  @stdin.puts(notif.to_json)
@@ -39,6 +39,8 @@ module MCPClient
39
39
  @next_id = 1
40
40
  @pending = {}
41
41
  @initialized = false
42
+ @server_info = nil
43
+ @capabilities = nil
42
44
  initialize_logger(logger)
43
45
  @max_retries = retries
44
46
  @retry_backoff = retry_backoff
@@ -46,6 +48,14 @@ module MCPClient
46
48
  @env = env || {}
47
49
  end
48
50
 
51
+ # Server info from the initialize response
52
+ # @return [Hash, nil] Server information
53
+ attr_reader :server_info
54
+
55
+ # Server capabilities from the initialize response
56
+ # @return [Hash, nil] Server capabilities
57
+ attr_reader :capabilities
58
+
49
59
  # Connect to the MCP server by launching the command process via stdin/stdout
50
60
  # @return [Boolean] true if connection was successful
51
61
  # @raise [MCPClient::Errors::ConnectionError] if connection fails
@@ -102,6 +112,178 @@ module MCPClient
102
112
  # Skip non-JSONRPC lines in the output stream
103
113
  end
104
114
 
115
+ # List all prompts available from the MCP server
116
+ # @return [Array<MCPClient::Prompt>] list of available prompts
117
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
118
+ # @raise [MCPClient::Errors::PromptGetError] for other errors during prompt listing
119
+ def list_prompts
120
+ ensure_initialized
121
+ req_id = next_id
122
+ req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'prompts/list', 'params' => {} }
123
+ send_request(req)
124
+ res = wait_response(req_id)
125
+ if (err = res['error'])
126
+ raise MCPClient::Errors::ServerError, err['message']
127
+ end
128
+
129
+ (res.dig('result', 'prompts') || []).map { |td| MCPClient::Prompt.from_json(td, server: self) }
130
+ rescue StandardError => e
131
+ raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
132
+ end
133
+
134
+ # Get a prompt with the given parameters
135
+ # @param prompt_name [String] the name of the prompt to get
136
+ # @param parameters [Hash] the parameters to pass to the prompt
137
+ # @return [Object] the result of the prompt interpolation
138
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
139
+ # @raise [MCPClient::Errors::PromptGetError] for other errors during prompt interpolation
140
+ def get_prompt(prompt_name, parameters)
141
+ ensure_initialized
142
+ req_id = next_id
143
+ # JSON-RPC method for getting a prompt
144
+ req = {
145
+ 'jsonrpc' => '2.0',
146
+ 'id' => req_id,
147
+ 'method' => 'prompts/get',
148
+ 'params' => { 'name' => prompt_name, 'arguments' => parameters }
149
+ }
150
+ send_request(req)
151
+ res = wait_response(req_id)
152
+ if (err = res['error'])
153
+ raise MCPClient::Errors::ServerError, err['message']
154
+ end
155
+
156
+ res['result']
157
+ rescue StandardError => e
158
+ raise MCPClient::Errors::PromptGetError, "Error calling prompt '#{prompt_name}': #{e.message}"
159
+ end
160
+
161
+ # List all resources available from the MCP server
162
+ # @param cursor [String, nil] optional cursor for pagination
163
+ # @return [Hash] result containing resources array and optional nextCursor
164
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
165
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
166
+ def list_resources(cursor: nil)
167
+ ensure_initialized
168
+ req_id = next_id
169
+ params = {}
170
+ params['cursor'] = cursor if cursor
171
+ req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => params }
172
+ send_request(req)
173
+ res = wait_response(req_id)
174
+ if (err = res['error'])
175
+ raise MCPClient::Errors::ServerError, err['message']
176
+ end
177
+
178
+ result = res['result'] || {}
179
+ resources = (result['resources'] || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
180
+ { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
181
+ rescue StandardError => e
182
+ raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
183
+ end
184
+
185
+ # Read a resource by its URI
186
+ # @param uri [String] the URI of the resource to read
187
+ # @return [Array<MCPClient::ResourceContent>] array of resource contents
188
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
189
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
190
+ def read_resource(uri)
191
+ ensure_initialized
192
+ req_id = next_id
193
+ # JSON-RPC method for reading a resource
194
+ req = {
195
+ 'jsonrpc' => '2.0',
196
+ 'id' => req_id,
197
+ 'method' => 'resources/read',
198
+ 'params' => { 'uri' => uri }
199
+ }
200
+ send_request(req)
201
+ res = wait_response(req_id)
202
+ if (err = res['error'])
203
+ raise MCPClient::Errors::ServerError, err['message']
204
+ end
205
+
206
+ result = res['result'] || {}
207
+ contents = result['contents'] || []
208
+ contents.map { |content| MCPClient::ResourceContent.from_json(content) }
209
+ rescue StandardError => e
210
+ raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
211
+ end
212
+
213
+ # List all resource templates available from the MCP server
214
+ # @param cursor [String, nil] optional cursor for pagination
215
+ # @return [Hash] result containing resourceTemplates array and optional nextCursor
216
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
217
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
218
+ def list_resource_templates(cursor: nil)
219
+ ensure_initialized
220
+ req_id = next_id
221
+ params = {}
222
+ params['cursor'] = cursor if cursor
223
+ req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/templates/list', 'params' => params }
224
+ send_request(req)
225
+ res = wait_response(req_id)
226
+ if (err = res['error'])
227
+ raise MCPClient::Errors::ServerError, err['message']
228
+ end
229
+
230
+ result = res['result'] || {}
231
+ templates = (result['resourceTemplates'] || []).map { |td| MCPClient::ResourceTemplate.from_json(td, server: self) }
232
+ { 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
233
+ rescue StandardError => e
234
+ raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
235
+ end
236
+
237
+ # Subscribe to resource updates
238
+ # @param uri [String] the URI of the resource to subscribe to
239
+ # @return [Boolean] true if subscription successful
240
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
241
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
242
+ def subscribe_resource(uri)
243
+ ensure_initialized
244
+ req_id = next_id
245
+ req = {
246
+ 'jsonrpc' => '2.0',
247
+ 'id' => req_id,
248
+ 'method' => 'resources/subscribe',
249
+ 'params' => { 'uri' => uri }
250
+ }
251
+ send_request(req)
252
+ res = wait_response(req_id)
253
+ if (err = res['error'])
254
+ raise MCPClient::Errors::ServerError, err['message']
255
+ end
256
+
257
+ true
258
+ rescue StandardError => e
259
+ raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
260
+ end
261
+
262
+ # Unsubscribe from resource updates
263
+ # @param uri [String] the URI of the resource to unsubscribe from
264
+ # @return [Boolean] true if unsubscription successful
265
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
266
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
267
+ def unsubscribe_resource(uri)
268
+ ensure_initialized
269
+ req_id = next_id
270
+ req = {
271
+ 'jsonrpc' => '2.0',
272
+ 'id' => req_id,
273
+ 'method' => 'resources/unsubscribe',
274
+ 'params' => { 'uri' => uri }
275
+ }
276
+ send_request(req)
277
+ res = wait_response(req_id)
278
+ if (err = res['error'])
279
+ raise MCPClient::Errors::ServerError, err['message']
280
+ end
281
+
282
+ true
283
+ rescue StandardError => e
284
+ raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
285
+ end
286
+
105
287
  # List all tools available from the MCP server
106
288
  # @return [Array<MCPClient::Tool>] list of available tools
107
289
  # @raise [MCPClient::Errors::ServerError] if server returns an error