ruby-mcp-client 0.6.2 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +316 -10
- data/lib/mcp_client/auth/oauth_provider.rb +514 -0
- data/lib/mcp_client/auth.rb +315 -0
- data/lib/mcp_client/client.rb +1 -1
- data/lib/mcp_client/config_parser.rb +73 -1
- data/lib/mcp_client/http_transport_base.rb +283 -0
- data/lib/mcp_client/json_rpc_common.rb +8 -10
- data/lib/mcp_client/oauth_client.rb +127 -0
- data/lib/mcp_client/server_factory.rb +42 -0
- data/lib/mcp_client/server_http/json_rpc_transport.rb +27 -0
- data/lib/mcp_client/server_http.rb +331 -0
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +5 -5
- data/lib/mcp_client/server_sse.rb +16 -8
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +1 -1
- data/lib/mcp_client/server_stdio.rb +1 -1
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +76 -0
- data/lib/mcp_client/server_streamable_http.rb +332 -0
- data/lib/mcp_client/tool.rb +4 -3
- data/lib/mcp_client/version.rb +4 -1
- data/lib/mcp_client.rb +61 -2
- metadata +10 -2
@@ -0,0 +1,283 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'json_rpc_common'
|
4
|
+
require_relative 'auth/oauth_provider'
|
5
|
+
|
6
|
+
module MCPClient
|
7
|
+
# Base module for HTTP-based JSON-RPC transports
|
8
|
+
# Contains common functionality shared between HTTP and Streamable HTTP transports
|
9
|
+
module HttpTransportBase
|
10
|
+
include JsonRpcCommon
|
11
|
+
|
12
|
+
# Generic JSON-RPC request: send method with params and return result
|
13
|
+
# @param method [String] JSON-RPC method name
|
14
|
+
# @param params [Hash] parameters for the request
|
15
|
+
# @return [Object] result from JSON-RPC response
|
16
|
+
# @raise [MCPClient::Errors::ConnectionError] if connection is not active
|
17
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
18
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
19
|
+
# @raise [MCPClient::Errors::ToolCallError] for other errors during request execution
|
20
|
+
def rpc_request(method, params = {})
|
21
|
+
ensure_connected
|
22
|
+
|
23
|
+
with_retry do
|
24
|
+
request_id = @mutex.synchronize { @request_id += 1 }
|
25
|
+
request = build_jsonrpc_request(method, params, request_id)
|
26
|
+
send_jsonrpc_request(request)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Send a JSON-RPC notification (no response expected)
|
31
|
+
# @param method [String] JSON-RPC method name
|
32
|
+
# @param params [Hash] parameters for the notification
|
33
|
+
# @return [void]
|
34
|
+
def rpc_notify(method, params = {})
|
35
|
+
ensure_connected
|
36
|
+
|
37
|
+
notif = build_jsonrpc_notification(method, params)
|
38
|
+
|
39
|
+
begin
|
40
|
+
send_http_request(notif)
|
41
|
+
rescue MCPClient::Errors::ServerError, MCPClient::Errors::ConnectionError, Faraday::ConnectionFailed => e
|
42
|
+
raise MCPClient::Errors::TransportError, "Failed to send notification: #{e.message}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Terminate the current session with the server
|
47
|
+
# Sends an HTTP DELETE request with the session ID to properly close the session
|
48
|
+
# @return [Boolean] true if termination was successful
|
49
|
+
# @raise [MCPClient::Errors::ConnectionError] if termination fails
|
50
|
+
def terminate_session
|
51
|
+
return true unless @session_id
|
52
|
+
|
53
|
+
conn = http_connection
|
54
|
+
|
55
|
+
begin
|
56
|
+
@logger.debug("Terminating session: #{@session_id}")
|
57
|
+
response = conn.delete(@endpoint) do |req|
|
58
|
+
# Apply base headers but prioritize session termination headers
|
59
|
+
@headers.each { |k, v| req.headers[k] = v }
|
60
|
+
req.headers['Mcp-Session-Id'] = @session_id
|
61
|
+
end
|
62
|
+
|
63
|
+
if response.success?
|
64
|
+
@logger.debug("Session terminated successfully: #{@session_id}")
|
65
|
+
@session_id = nil
|
66
|
+
true
|
67
|
+
else
|
68
|
+
@logger.warn("Session termination failed with HTTP #{response.status}")
|
69
|
+
@session_id = nil # Clear session ID even on HTTP error
|
70
|
+
false
|
71
|
+
end
|
72
|
+
rescue Faraday::Error => e
|
73
|
+
@logger.warn("Session termination request failed: #{e.message}")
|
74
|
+
# Clear session ID even if termination request failed
|
75
|
+
@session_id = nil
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validate session ID format for security
|
81
|
+
# @param session_id [String] the session ID to validate
|
82
|
+
# @return [Boolean] true if session ID is valid
|
83
|
+
def valid_session_id?(session_id)
|
84
|
+
return false unless session_id.is_a?(String)
|
85
|
+
return false if session_id.empty?
|
86
|
+
|
87
|
+
# Session ID should be alphanumeric with optional hyphens and underscores
|
88
|
+
# Length should be reasonable (8-128 characters)
|
89
|
+
session_id.match?(/\A[a-zA-Z0-9\-_]{8,128}\z/)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Validate the server's base URL for security
|
93
|
+
# @param url [String] the URL to validate
|
94
|
+
# @return [Boolean] true if URL is considered safe
|
95
|
+
def valid_server_url?(url)
|
96
|
+
return false unless url.is_a?(String)
|
97
|
+
|
98
|
+
uri = URI.parse(url)
|
99
|
+
|
100
|
+
# Only allow HTTP and HTTPS protocols
|
101
|
+
return false unless %w[http https].include?(uri.scheme)
|
102
|
+
|
103
|
+
# Must have a host
|
104
|
+
return false if uri.host.nil? || uri.host.empty?
|
105
|
+
|
106
|
+
# Don't allow localhost binding to all interfaces in production
|
107
|
+
if uri.host == '0.0.0.0'
|
108
|
+
@logger.warn('Server URL uses 0.0.0.0 which may be insecure. Consider using 127.0.0.1 for localhost.')
|
109
|
+
end
|
110
|
+
|
111
|
+
true
|
112
|
+
rescue URI::InvalidURIError
|
113
|
+
false
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Generate initialization parameters for HTTP MCP protocol
|
119
|
+
# @return [Hash] the initialization parameters
|
120
|
+
def initialization_params
|
121
|
+
{
|
122
|
+
'protocolVersion' => MCPClient::HTTP_PROTOCOL_VERSION,
|
123
|
+
'capabilities' => {},
|
124
|
+
'clientInfo' => { 'name' => 'ruby-mcp-client', 'version' => MCPClient::VERSION }
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Perform JSON-RPC initialize handshake with the MCP server
|
129
|
+
# @return [void]
|
130
|
+
# @raise [MCPClient::Errors::ConnectionError] if initialization fails
|
131
|
+
def perform_initialize
|
132
|
+
request_id = @mutex.synchronize { @request_id += 1 }
|
133
|
+
json_rpc_request = build_jsonrpc_request('initialize', initialization_params, request_id)
|
134
|
+
@logger.debug("Performing initialize RPC: #{json_rpc_request}")
|
135
|
+
|
136
|
+
result = send_jsonrpc_request(json_rpc_request)
|
137
|
+
return unless result.is_a?(Hash)
|
138
|
+
|
139
|
+
@server_info = result['serverInfo']
|
140
|
+
@capabilities = result['capabilities']
|
141
|
+
end
|
142
|
+
|
143
|
+
# Send a JSON-RPC request to the server and wait for result
|
144
|
+
# @param request [Hash] the JSON-RPC request
|
145
|
+
# @return [Hash] the result of the request
|
146
|
+
# @raise [MCPClient::Errors::ConnectionError] if connection fails
|
147
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
148
|
+
# @raise [MCPClient::Errors::ToolCallError] for other errors during request execution
|
149
|
+
def send_jsonrpc_request(request)
|
150
|
+
@logger.debug("Sending JSON-RPC request: #{request.to_json}")
|
151
|
+
|
152
|
+
begin
|
153
|
+
response = send_http_request(request)
|
154
|
+
parse_response(response)
|
155
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
156
|
+
raise
|
157
|
+
rescue JSON::ParserError => e
|
158
|
+
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
|
159
|
+
rescue Errno::ECONNREFUSED => e
|
160
|
+
raise MCPClient::Errors::ConnectionError, "Server connection lost: #{e.message}"
|
161
|
+
rescue StandardError => e
|
162
|
+
method_name = request['method']
|
163
|
+
raise MCPClient::Errors::ToolCallError, "Error executing request '#{method_name}': #{e.message}"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Send an HTTP request to the server
|
168
|
+
# @param request [Hash] the JSON-RPC request
|
169
|
+
# @return [Faraday::Response] the HTTP response
|
170
|
+
# @raise [MCPClient::Errors::ConnectionError] if connection fails
|
171
|
+
def send_http_request(request)
|
172
|
+
conn = http_connection
|
173
|
+
|
174
|
+
begin
|
175
|
+
response = conn.post(@endpoint) do |req|
|
176
|
+
apply_request_headers(req, request)
|
177
|
+
req.body = request.to_json
|
178
|
+
end
|
179
|
+
|
180
|
+
handle_http_error_response(response) unless response.success?
|
181
|
+
handle_successful_response(response, request)
|
182
|
+
|
183
|
+
log_response(response)
|
184
|
+
response
|
185
|
+
rescue Faraday::UnauthorizedError, Faraday::ForbiddenError => e
|
186
|
+
handle_auth_error(e)
|
187
|
+
rescue Faraday::ConnectionFailed => e
|
188
|
+
raise MCPClient::Errors::ConnectionError, "Server connection lost: #{e.message}"
|
189
|
+
rescue Faraday::Error => e
|
190
|
+
raise MCPClient::Errors::TransportError, "HTTP request failed: #{e.message}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Apply headers to the HTTP request (can be overridden by subclasses)
|
195
|
+
# @param req [Faraday::Request] HTTP request
|
196
|
+
# @param _request [Hash] JSON-RPC request
|
197
|
+
def apply_request_headers(req, _request)
|
198
|
+
# Apply all headers including custom ones
|
199
|
+
@headers.each { |k, v| req.headers[k] = v }
|
200
|
+
|
201
|
+
# Apply OAuth authorization if available
|
202
|
+
@logger.debug("OAuth provider present: #{@oauth_provider ? 'yes' : 'no'}")
|
203
|
+
@oauth_provider&.apply_authorization(req)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Handle successful HTTP response (can be overridden by subclasses)
|
207
|
+
# @param response [Faraday::Response] HTTP response
|
208
|
+
# @param _request [Hash] JSON-RPC request
|
209
|
+
def handle_successful_response(response, _request)
|
210
|
+
# Default: no additional handling
|
211
|
+
end
|
212
|
+
|
213
|
+
# Handle authentication errors
|
214
|
+
# @param error [Faraday::UnauthorizedError, Faraday::ForbiddenError] Auth error
|
215
|
+
# @raise [MCPClient::Errors::ConnectionError] Connection error
|
216
|
+
def handle_auth_error(error)
|
217
|
+
# Handle OAuth authorization challenges
|
218
|
+
if error.response && @oauth_provider
|
219
|
+
resource_metadata = @oauth_provider.handle_unauthorized_response(error.response)
|
220
|
+
if resource_metadata
|
221
|
+
@logger.debug('Received OAuth challenge, discovered resource metadata')
|
222
|
+
# Re-raise the error to trigger OAuth flow in calling code
|
223
|
+
raise MCPClient::Errors::ConnectionError, "OAuth authorization required: HTTP #{error.response[:status]}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
error_status = error.response ? error.response[:status] : 'unknown'
|
228
|
+
raise MCPClient::Errors::ConnectionError, "Authorization failed: HTTP #{error_status}"
|
229
|
+
end
|
230
|
+
|
231
|
+
# Handle HTTP error responses
|
232
|
+
# @param response [Faraday::Response] the error response
|
233
|
+
# @raise [MCPClient::Errors::ConnectionError] for auth errors
|
234
|
+
# @raise [MCPClient::Errors::ServerError] for server errors
|
235
|
+
def handle_http_error_response(response)
|
236
|
+
reason = response.respond_to?(:reason_phrase) ? response.reason_phrase : ''
|
237
|
+
reason = reason.to_s.strip
|
238
|
+
reason_text = reason.empty? ? '' : " #{reason}"
|
239
|
+
|
240
|
+
case response.status
|
241
|
+
when 401, 403
|
242
|
+
raise MCPClient::Errors::ConnectionError, "Authorization failed: HTTP #{response.status}"
|
243
|
+
when 400..499
|
244
|
+
raise MCPClient::Errors::ServerError, "Client error: HTTP #{response.status}#{reason_text}"
|
245
|
+
when 500..599
|
246
|
+
raise MCPClient::Errors::ServerError, "Server error: HTTP #{response.status}#{reason_text}"
|
247
|
+
else
|
248
|
+
raise MCPClient::Errors::ServerError, "HTTP error: #{response.status}#{reason_text}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Get or create HTTP connection
|
253
|
+
# @return [Faraday::Connection] the HTTP connection
|
254
|
+
def http_connection
|
255
|
+
@http_connection ||= create_http_connection
|
256
|
+
end
|
257
|
+
|
258
|
+
# Create a Faraday connection for HTTP requests
|
259
|
+
# @return [Faraday::Connection] the configured connection
|
260
|
+
def create_http_connection
|
261
|
+
Faraday.new(url: @base_url) do |f|
|
262
|
+
f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
|
263
|
+
f.options.open_timeout = @read_timeout
|
264
|
+
f.options.timeout = @read_timeout
|
265
|
+
f.adapter Faraday.default_adapter
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Log HTTP response (to be overridden by specific transports)
|
270
|
+
# @param response [Faraday::Response] the HTTP response
|
271
|
+
def log_response(response)
|
272
|
+
@logger.debug("Received HTTP response: #{response.status} #{response.body}")
|
273
|
+
end
|
274
|
+
|
275
|
+
# Parse HTTP response (to be implemented by specific transports)
|
276
|
+
# @param response [Faraday::Response] the HTTP response
|
277
|
+
# @return [Hash] the parsed result
|
278
|
+
# @raise [NotImplementedError] if not implemented by concrete transport
|
279
|
+
def parse_response(response)
|
280
|
+
raise NotImplementedError, 'Subclass must implement parse_response'
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
@@ -40,10 +40,10 @@ module MCPClient
|
|
40
40
|
# @return [Hash] the JSON-RPC request object
|
41
41
|
def build_jsonrpc_request(method, params, id)
|
42
42
|
{
|
43
|
-
jsonrpc
|
44
|
-
id
|
45
|
-
method
|
46
|
-
params
|
43
|
+
'jsonrpc' => '2.0',
|
44
|
+
'id' => id,
|
45
|
+
'method' => method,
|
46
|
+
'params' => params
|
47
47
|
}
|
48
48
|
end
|
49
49
|
|
@@ -53,9 +53,9 @@ module MCPClient
|
|
53
53
|
# @return [Hash] the JSON-RPC notification object
|
54
54
|
def build_jsonrpc_notification(method, params)
|
55
55
|
{
|
56
|
-
jsonrpc
|
57
|
-
method
|
58
|
-
params
|
56
|
+
'jsonrpc' => '2.0',
|
57
|
+
'method' => method,
|
58
|
+
'params' => params
|
59
59
|
}
|
60
60
|
end
|
61
61
|
|
@@ -74,9 +74,7 @@ module MCPClient
|
|
74
74
|
# @return [Object] the result field from the response
|
75
75
|
# @raise [MCPClient::Errors::ServerError] if the response contains an error
|
76
76
|
def process_jsonrpc_response(response)
|
77
|
-
|
78
|
-
raise MCPClient::Errors::ServerError, err['message']
|
79
|
-
end
|
77
|
+
raise MCPClient::Errors::ServerError, response['error']['message'] if response['error']
|
80
78
|
|
81
79
|
response['result']
|
82
80
|
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'auth/oauth_provider'
|
4
|
+
require_relative 'server_http'
|
5
|
+
require_relative 'server_streamable_http'
|
6
|
+
|
7
|
+
module MCPClient
|
8
|
+
# Utility class for creating OAuth-enabled MCP clients
|
9
|
+
class OAuthClient
|
10
|
+
# Create an OAuth-enabled HTTP server
|
11
|
+
# @param server_url [String] The MCP server URL
|
12
|
+
# @param options [Hash] Configuration options
|
13
|
+
# @option options [String] :redirect_uri OAuth redirect URI (default: 'http://localhost:8080/callback')
|
14
|
+
# @option options [String, nil] :scope OAuth scope
|
15
|
+
# @option options [String] :endpoint JSON-RPC endpoint path (default: '/rpc')
|
16
|
+
# @option options [Hash] :headers Additional headers to include in requests
|
17
|
+
# @option options [Integer] :read_timeout Read timeout in seconds (default: 30)
|
18
|
+
# @option options [Integer] :retries Retry attempts on transient errors (default: 3)
|
19
|
+
# @option options [Numeric] :retry_backoff Base delay for exponential backoff (default: 1)
|
20
|
+
# @option options [String, nil] :name Optional name for this server
|
21
|
+
# @option options [Logger, nil] :logger Optional logger
|
22
|
+
# @option options [Object, nil] :storage Storage backend for OAuth tokens and client info
|
23
|
+
# @return [ServerHTTP] OAuth-enabled HTTP server
|
24
|
+
def self.create_http_server(server_url:, **options)
|
25
|
+
opts = default_server_options.merge(options)
|
26
|
+
|
27
|
+
oauth_provider = Auth::OAuthProvider.new(
|
28
|
+
server_url: server_url,
|
29
|
+
redirect_uri: opts[:redirect_uri],
|
30
|
+
scope: opts[:scope],
|
31
|
+
logger: opts[:logger],
|
32
|
+
storage: opts[:storage]
|
33
|
+
)
|
34
|
+
|
35
|
+
ServerHTTP.new(
|
36
|
+
base_url: server_url,
|
37
|
+
endpoint: opts[:endpoint],
|
38
|
+
headers: opts[:headers],
|
39
|
+
read_timeout: opts[:read_timeout],
|
40
|
+
retries: opts[:retries],
|
41
|
+
retry_backoff: opts[:retry_backoff],
|
42
|
+
name: opts[:name],
|
43
|
+
logger: opts[:logger],
|
44
|
+
oauth_provider: oauth_provider
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create an OAuth-enabled Streamable HTTP server
|
49
|
+
# @param server_url [String] The MCP server URL
|
50
|
+
# @param options [Hash] Configuration options (same as create_http_server)
|
51
|
+
# @return [ServerStreamableHTTP] OAuth-enabled Streamable HTTP server
|
52
|
+
def self.create_streamable_http_server(server_url:, **options)
|
53
|
+
opts = default_server_options.merge(options)
|
54
|
+
|
55
|
+
oauth_provider = Auth::OAuthProvider.new(
|
56
|
+
server_url: server_url,
|
57
|
+
redirect_uri: opts[:redirect_uri],
|
58
|
+
scope: opts[:scope],
|
59
|
+
logger: opts[:logger],
|
60
|
+
storage: opts[:storage]
|
61
|
+
)
|
62
|
+
|
63
|
+
ServerStreamableHTTP.new(
|
64
|
+
base_url: server_url,
|
65
|
+
endpoint: opts[:endpoint],
|
66
|
+
headers: opts[:headers],
|
67
|
+
read_timeout: opts[:read_timeout],
|
68
|
+
retries: opts[:retries],
|
69
|
+
retry_backoff: opts[:retry_backoff],
|
70
|
+
name: opts[:name],
|
71
|
+
logger: opts[:logger],
|
72
|
+
oauth_provider: oauth_provider
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Perform OAuth authorization flow for a server
|
77
|
+
# This is a helper method that can be used to manually perform the OAuth flow
|
78
|
+
# @param server [ServerHTTP, ServerStreamableHTTP] The OAuth-enabled server
|
79
|
+
# @return [String] Authorization URL to redirect user to
|
80
|
+
# @raise [ArgumentError] if server doesn't have OAuth provider
|
81
|
+
def self.start_oauth_flow(server)
|
82
|
+
oauth_provider = server.instance_variable_get(:@oauth_provider)
|
83
|
+
raise ArgumentError, 'Server does not have OAuth provider configured' unless oauth_provider
|
84
|
+
|
85
|
+
oauth_provider.start_authorization_flow
|
86
|
+
end
|
87
|
+
|
88
|
+
# Complete OAuth authorization flow with authorization code
|
89
|
+
# @param server [ServerHTTP, ServerStreamableHTTP] The OAuth-enabled server
|
90
|
+
# @param code [String] Authorization code from callback
|
91
|
+
# @param state [String] State parameter from callback
|
92
|
+
# @return [Auth::Token] Access token
|
93
|
+
# @raise [ArgumentError] if server doesn't have OAuth provider
|
94
|
+
def self.complete_oauth_flow(server, code, state)
|
95
|
+
oauth_provider = server.instance_variable_get(:@oauth_provider)
|
96
|
+
raise ArgumentError, 'Server does not have OAuth provider configured' unless oauth_provider
|
97
|
+
|
98
|
+
oauth_provider.complete_authorization_flow(code, state)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Check if server has a valid OAuth access token
|
102
|
+
# @param server [ServerHTTP, ServerStreamableHTTP] The OAuth-enabled server
|
103
|
+
# @return [Boolean] true if server has valid access token
|
104
|
+
def self.valid_token?(server)
|
105
|
+
oauth_provider = server.instance_variable_get(:@oauth_provider)
|
106
|
+
return false unless oauth_provider
|
107
|
+
|
108
|
+
token = oauth_provider.access_token
|
109
|
+
!!(token && !token.expired?)
|
110
|
+
end
|
111
|
+
|
112
|
+
private_class_method def self.default_server_options
|
113
|
+
{
|
114
|
+
redirect_uri: 'http://localhost:8080/callback',
|
115
|
+
scope: nil,
|
116
|
+
endpoint: '/rpc',
|
117
|
+
headers: {},
|
118
|
+
read_timeout: 30,
|
119
|
+
retries: 3,
|
120
|
+
retry_backoff: 1,
|
121
|
+
name: nil,
|
122
|
+
logger: nil,
|
123
|
+
storage: nil
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -15,6 +15,10 @@ module MCPClient
|
|
15
15
|
create_stdio_server(config, logger_to_use)
|
16
16
|
when 'sse'
|
17
17
|
create_sse_server(config, logger_to_use)
|
18
|
+
when 'http'
|
19
|
+
create_http_server(config, logger_to_use)
|
20
|
+
when 'streamable_http'
|
21
|
+
create_streamable_http_server(config, logger_to_use)
|
18
22
|
else
|
19
23
|
raise ArgumentError, "Unknown server type: #{config[:type]}"
|
20
24
|
end
|
@@ -57,6 +61,44 @@ module MCPClient
|
|
57
61
|
)
|
58
62
|
end
|
59
63
|
|
64
|
+
# Create an HTTP-based server
|
65
|
+
# @param config [Hash] server configuration
|
66
|
+
# @param logger [Logger, nil] logger to use
|
67
|
+
# @return [MCPClient::ServerHTTP] server instance
|
68
|
+
def self.create_http_server(config, logger)
|
69
|
+
# Handle both :url and :base_url (config parser uses :url)
|
70
|
+
base_url = config[:base_url] || config[:url]
|
71
|
+
MCPClient::ServerHTTP.new(
|
72
|
+
base_url: base_url,
|
73
|
+
endpoint: config[:endpoint] || '/rpc',
|
74
|
+
headers: config[:headers] || {},
|
75
|
+
read_timeout: config[:read_timeout] || 30,
|
76
|
+
retries: config[:retries] || 3,
|
77
|
+
retry_backoff: config[:retry_backoff] || 1,
|
78
|
+
name: config[:name],
|
79
|
+
logger: logger
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Create a Streamable HTTP-based server
|
84
|
+
# @param config [Hash] server configuration
|
85
|
+
# @param logger [Logger, nil] logger to use
|
86
|
+
# @return [MCPClient::ServerStreamableHTTP] server instance
|
87
|
+
def self.create_streamable_http_server(config, logger)
|
88
|
+
# Handle both :url and :base_url (config parser uses :url)
|
89
|
+
base_url = config[:base_url] || config[:url]
|
90
|
+
MCPClient::ServerStreamableHTTP.new(
|
91
|
+
base_url: base_url,
|
92
|
+
endpoint: config[:endpoint] || '/rpc',
|
93
|
+
headers: config[:headers] || {},
|
94
|
+
read_timeout: config[:read_timeout] || 30,
|
95
|
+
retries: config[:retries] || 3,
|
96
|
+
retry_backoff: config[:retry_backoff] || 1,
|
97
|
+
name: config[:name],
|
98
|
+
logger: logger
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
60
102
|
# Prepare command by combining command and args
|
61
103
|
# @param config [Hash] server configuration
|
62
104
|
# @return [String, Array] prepared command
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../http_transport_base'
|
4
|
+
|
5
|
+
module MCPClient
|
6
|
+
class ServerHTTP
|
7
|
+
# JSON-RPC request/notification plumbing for HTTP transport
|
8
|
+
module JsonRpcTransport
|
9
|
+
include HttpTransportBase
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# Parse an HTTP JSON-RPC response
|
14
|
+
# @param response [Faraday::Response] the HTTP response
|
15
|
+
# @return [Hash] the parsed result
|
16
|
+
# @raise [MCPClient::Errors::TransportError] if parsing fails
|
17
|
+
# @raise [MCPClient::Errors::ServerError] if the response contains an error
|
18
|
+
def parse_response(response)
|
19
|
+
body = response.body.strip
|
20
|
+
data = JSON.parse(body)
|
21
|
+
process_jsonrpc_response(data)
|
22
|
+
rescue JSON::ParserError => e
|
23
|
+
raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|