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.
- checksums.yaml +4 -4
- data/README.md +128 -28
- data/lib/mcp_client/client.rb +182 -5
- data/lib/mcp_client/errors.rb +18 -0
- data/lib/mcp_client/prompt.rb +41 -0
- data/lib/mcp_client/resource.rb +61 -0
- data/lib/mcp_client/server_base.rb +27 -0
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +1 -0
- data/lib/mcp_client/server_sse.rb +157 -1
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +1 -0
- data/lib/mcp_client/server_stdio.rb +91 -0
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +26 -12
- data/lib/mcp_client/server_streamable_http.rb +422 -9
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +2 -0
- metadata +4 -2
@@ -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
|
-
|
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
|
48
|
+
# Extract JSON data from SSE format, processing events separately
|
49
49
|
# SSE format: event: message\nid: 123\ndata: {...}\n\n
|
50
|
-
|
51
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
65
|
+
current_event[:id] = line.sub(/^id:\s*/, '').strip
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
|
-
|
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
|
65
|
-
if
|
66
|
-
@last_event_id =
|
67
|
-
@logger.debug("Tracking event ID for resumability: #{
|
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
|