ruby-mcp-client 0.7.2 → 0.8.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.
@@ -33,6 +33,33 @@ module MCPClient
33
33
  raise NotImplementedError, 'Subclasses must implement call_tool'
34
34
  end
35
35
 
36
+ # List all prompts available from the MCP server
37
+ # @return [Array<MCPClient::Prompt>] list of available prompts
38
+ def list_prompts
39
+ raise NotImplementedError, 'Subclasses must implement list_prompts'
40
+ end
41
+
42
+ # Get a prompt with the given parameters
43
+ # @param prompt_name [String] the name of the prompt to get
44
+ # @param parameters [Hash] the parameters to pass to the prompt
45
+ # @return [Object] the result of the prompt interpolation
46
+ def get_prompt(prompt_name, parameters)
47
+ raise NotImplementedError, 'Subclasses must implement get_prompt'
48
+ end
49
+
50
+ # List all resources available from the MCP server
51
+ # @return [Array<MCPClient::Resource>] list of available resources
52
+ def list_resources
53
+ raise NotImplementedError, 'Subclasses must implement list_resources'
54
+ end
55
+
56
+ # Read a resource by its URI
57
+ # @param uri [String] the URI of the resource to read
58
+ # @return [Object] the resource contents
59
+ def read_resource(uri)
60
+ raise NotImplementedError, 'Subclasses must implement read_resource'
61
+ end
62
+
36
63
  # Clean up the server connection
37
64
  def cleanup
38
65
  raise NotImplementedError, 'Subclasses must implement cleanup'
@@ -7,6 +7,7 @@ module MCPClient
7
7
  # JSON-RPC request/notification plumbing for SSE transport
8
8
  module JsonRpcTransport
9
9
  include JsonRpcCommon
10
+
10
11
  # Generic JSON-RPC request: send method with params and return result
11
12
  # @param method [String] JSON-RPC method name
12
13
  # @param params [Hash] parameters for the request
@@ -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,104 @@ 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
+ # @return [Array<MCPClient::Resource>] list of available resources
165
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
166
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
167
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
168
+ def list_resources
169
+ @mutex.synchronize do
170
+ return @resources if @resources
171
+ end
172
+
173
+ begin
174
+ ensure_initialized
175
+
176
+ resources_data = request_resources_list
177
+ @mutex.synchronize do
178
+ @resources = resources_data.map do |resource_data|
179
+ MCPClient::Resource.from_json(resource_data, server: self)
180
+ end
181
+ end
182
+
183
+ @mutex.synchronize { @resources }
184
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
185
+ # Re-raise these errors directly
186
+ raise
187
+ rescue StandardError => e
188
+ raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
189
+ end
190
+ end
191
+
192
+ # Read a resource by its URI
193
+ # @param uri [String] the URI of the resource to read
194
+ # @return [Object] the resource contents
195
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
196
+ # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
197
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
198
+ # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
199
+ def read_resource(uri)
200
+ rpc_request('resources/read', {
201
+ uri: uri
202
+ })
203
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
204
+ # Re-raise connection/transport errors directly to match test expectations
205
+ raise
206
+ rescue StandardError => e
207
+ # For all other errors, wrap in ResourceReadError
208
+ raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
209
+ end
210
+
107
211
  # List all tools available from the MCP server
108
212
  # @return [Array<MCPClient::Tool>] list of available tools
109
213
  # @raise [MCPClient::Errors::ServerError] if server returns an error
@@ -465,6 +569,58 @@ module MCPClient
465
569
  raise MCPClient::Errors::ConnectionError, "Authorization failed: #{error_message}"
466
570
  end
467
571
 
572
+ # Request the prompts list using JSON-RPC
573
+ # @return [Array<Hash>] the prompts data
574
+ # @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
575
+ # @private
576
+ def request_prompts_list
577
+ @mutex.synchronize do
578
+ return @prompts_data if @prompts_data
579
+ end
580
+
581
+ result = rpc_request('prompts/list')
582
+
583
+ if result && result['prompts']
584
+ @mutex.synchronize do
585
+ @prompts_data = result['prompts']
586
+ end
587
+ return @mutex.synchronize { @prompts_data.dup }
588
+ elsif result
589
+ @mutex.synchronize do
590
+ @prompts_data = result
591
+ end
592
+ return @mutex.synchronize { @prompts_data.dup }
593
+ end
594
+
595
+ raise MCPClient::Errors::PromptGetError, 'Failed to get prompts list from JSON-RPC request'
596
+ end
597
+
598
+ # Request the resources list using JSON-RPC
599
+ # @return [Array<Hash>] the resources data
600
+ # @raise [MCPClient::Errors::ResourceReadError] if resources list retrieval fails
601
+ # @private
602
+ def request_resources_list
603
+ @mutex.synchronize do
604
+ return @resources_data if @resources_data
605
+ end
606
+
607
+ result = rpc_request('resources/list')
608
+
609
+ if result && result['resources']
610
+ @mutex.synchronize do
611
+ @resources_data = result['resources']
612
+ end
613
+ return @mutex.synchronize { @resources_data.dup }
614
+ elsif result
615
+ @mutex.synchronize do
616
+ @resources_data = result
617
+ end
618
+ return @mutex.synchronize { @resources_data.dup }
619
+ end
620
+
621
+ raise MCPClient::Errors::ResourceReadError, 'Failed to get resources list from JSON-RPC request'
622
+ end
623
+
468
624
  # Request the tools list using JSON-RPC
469
625
  # @return [Array<Hash>] the tools data
470
626
  # @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
@@ -102,6 +102,97 @@ module MCPClient
102
102
  # Skip non-JSONRPC lines in the output stream
103
103
  end
104
104
 
105
+ # List all prompts available from the MCP server
106
+ # @return [Array<MCPClient::Prompt>] list of available prompts
107
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
108
+ # @raise [MCPClient::Errors::PromptGetError] for other errors during prompt listing
109
+ def list_prompts
110
+ ensure_initialized
111
+ req_id = next_id
112
+ req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'prompts/list', 'params' => {} }
113
+ send_request(req)
114
+ res = wait_response(req_id)
115
+ if (err = res['error'])
116
+ raise MCPClient::Errors::ServerError, err['message']
117
+ end
118
+
119
+ (res.dig('result', 'prompts') || []).map { |td| MCPClient::Prompt.from_json(td, server: self) }
120
+ rescue StandardError => e
121
+ raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
122
+ end
123
+
124
+ # Get a prompt with the given parameters
125
+ # @param prompt_name [String] the name of the prompt to get
126
+ # @param parameters [Hash] the parameters to pass to the prompt
127
+ # @return [Object] the result of the prompt interpolation
128
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
129
+ # @raise [MCPClient::Errors::PromptGetError] for other errors during prompt interpolation
130
+ def get_prompt(prompt_name, parameters)
131
+ ensure_initialized
132
+ req_id = next_id
133
+ # JSON-RPC method for getting a prompt
134
+ req = {
135
+ 'jsonrpc' => '2.0',
136
+ 'id' => req_id,
137
+ 'method' => 'prompts/get',
138
+ 'params' => { 'name' => prompt_name, 'arguments' => parameters }
139
+ }
140
+ send_request(req)
141
+ res = wait_response(req_id)
142
+ if (err = res['error'])
143
+ raise MCPClient::Errors::ServerError, err['message']
144
+ end
145
+
146
+ res['result']
147
+ rescue StandardError => e
148
+ raise MCPClient::Errors::PromptGetError, "Error calling prompt '#{prompt_name}': #{e.message}"
149
+ end
150
+
151
+ # List all resources available from the MCP server
152
+ # @return [Array<MCPClient::Resource>] list of available resources
153
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
154
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
155
+ def list_resources
156
+ ensure_initialized
157
+ req_id = next_id
158
+ req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => {} }
159
+ send_request(req)
160
+ res = wait_response(req_id)
161
+ if (err = res['error'])
162
+ raise MCPClient::Errors::ServerError, err['message']
163
+ end
164
+
165
+ (res.dig('result', 'resources') || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
166
+ rescue StandardError => e
167
+ raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
168
+ end
169
+
170
+ # Read a resource by its URI
171
+ # @param uri [String] the URI of the resource to read
172
+ # @return [Object] the resource contents
173
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
174
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
175
+ def read_resource(uri)
176
+ ensure_initialized
177
+ req_id = next_id
178
+ # JSON-RPC method for reading a resource
179
+ req = {
180
+ 'jsonrpc' => '2.0',
181
+ 'id' => req_id,
182
+ 'method' => 'resources/read',
183
+ 'params' => { 'uri' => uri }
184
+ }
185
+ send_request(req)
186
+ res = wait_response(req_id)
187
+ if (err = res['error'])
188
+ raise MCPClient::Errors::ServerError, err['message']
189
+ end
190
+
191
+ res['result']
192
+ rescue StandardError => e
193
+ raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
194
+ end
195
+
105
196
  # List all tools available from the MCP server
106
197
  # @return [Array<MCPClient::Tool>] list of available tools
107
198
  # @raise [MCPClient::Errors::ServerError] if server returns an error
@@ -45,30 +45,44 @@ module MCPClient
45
45
  # @return [Hash] the parsed JSON data
46
46
  # @raise [MCPClient::Errors::TransportError] if no data found in SSE response
47
47
  def parse_sse_response(sse_body)
48
- # Extract JSON data and event ID from SSE format
48
+ # Extract JSON data from SSE format, processing events separately
49
49
  # SSE format: event: message\nid: 123\ndata: {...}\n\n
50
- data_lines = []
51
- event_id = nil
50
+ events = []
51
+ current_event = { type: nil, data_lines: [], id: nil }
52
52
 
53
53
  sse_body.lines.each do |line|
54
54
  line = line.strip
55
- if line.start_with?('data:')
56
- data_lines << line.sub(/^data:\s*/, '').strip
55
+
56
+ if line.empty?
57
+ # Empty line marks end of an event
58
+ events << current_event.dup if current_event[:type] && !current_event[:data_lines].empty?
59
+ current_event = { type: nil, data_lines: [], id: nil }
60
+ elsif line.start_with?('event:')
61
+ current_event[:type] = line.sub(/^event:\s*/, '').strip
62
+ elsif line.start_with?('data:')
63
+ current_event[:data_lines] << line.sub(/^data:\s*/, '').strip
57
64
  elsif line.start_with?('id:')
58
- event_id = line.sub(/^id:\s*/, '').strip
65
+ current_event[:id] = line.sub(/^id:\s*/, '').strip
59
66
  end
60
67
  end
61
68
 
62
- raise MCPClient::Errors::TransportError, 'No data found in SSE response' if data_lines.empty?
69
+ # Handle last event if no trailing empty line
70
+ events << current_event if current_event[:type] && !current_event[:data_lines].empty?
71
+
72
+ # Find the first 'message' event which contains the JSON-RPC response
73
+ message_event = events.find { |e| e[:type] == 'message' }
74
+
75
+ raise MCPClient::Errors::TransportError, 'No data found in SSE response' unless message_event
76
+ raise MCPClient::Errors::TransportError, 'No data found in message event' if message_event[:data_lines].empty?
63
77
 
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}")
78
+ # Track the event ID for resumability
79
+ if message_event[:id] && !message_event[:id].empty?
80
+ @last_event_id = message_event[:id]
81
+ @logger.debug("Tracking event ID for resumability: #{message_event[:id]}")
68
82
  end
69
83
 
70
84
  # Join multiline data fields according to SSE spec
71
- json_data = data_lines.join("\n")
85
+ json_data = message_event[:data_lines].join("\n")
72
86
  JSON.parse(json_data)
73
87
  end
74
88
  end