Class: Encom::Client
- Inherits:
-
Object
- Object
- Encom::Client
- Defined in:
- lib/encom/client.rb
Defined Under Namespace
Classes: ConnectionError, ProtocolVersionError, RequestTimeoutError, ToolError
Constant Summary collapse
- LATEST_PROTOCOL_VERSION =
'2024-11-05'- SUPPORTED_PROTOCOL_VERSIONS =
[ LATEST_PROTOCOL_VERSION # Add more supported versions as they're developed ].freeze
Instance Attribute Summary collapse
-
#initialized ⇒ Object
readonly
Returns the value of attribute initialized.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#protocol_version ⇒ Object
readonly
Returns the value of attribute protocol_version.
-
#responses ⇒ Object
readonly
Returns the value of attribute responses.
-
#server_capabilities ⇒ Object
readonly
Returns the value of attribute server_capabilities.
-
#server_info ⇒ Object
readonly
Returns the value of attribute server_info.
-
#tool_responses ⇒ Object
readonly
Returns the value of attribute tool_responses.
-
#version ⇒ Object
readonly
Returns the value of attribute version.
Instance Method Summary collapse
-
#call_tool(name:, arguments:, timeout: 5) ⇒ Hash
Call a tool on the server.
- #close ⇒ Object
- #connect(transport) ⇒ Object
-
#get_tool_result(request_id) ⇒ Hash?
Get the result of a tool call.
-
#get_tools(request_id) ⇒ Array<Hash>?
Get the list of tools from a previous list_tools request.
- #handle_error(response) ⇒ Object
- #handle_initialize_result(response) ⇒ Object
- #handle_response(data) ⇒ Object
- #handle_transport_error(error) ⇒ Object
-
#initialize(name:, version:, capabilities:) ⇒ Client
constructor
A new instance of Client.
-
#list_tools(params = nil, timeout = 5) ⇒ Array<Hash>
List available tools from the server.
- #notification(notification_data) ⇒ Object
-
#on_error(&block) ⇒ Object
Register a callback for error handling.
- #request(request_data) ⇒ Object
- #trigger_error(error) ⇒ Object
-
#wait_for_response(id, timeout = 5) ⇒ Hash
Wait for a response with a specific ID, with timeout.
Constructor Details
#initialize(name:, version:, capabilities:) ⇒ Client
Returns a new instance of Client.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/encom/client.rb', line 21 def initialize(name:, version:, capabilities:) @name = name @version = version @capabilities = capabilities @message_id = 0 @responses = [] @tool_responses = {} @pending_requests = {} @response_mutex = Mutex.new @response_condition = ConditionVariable.new @initialized = false @closing = false @error_handlers = [] @first_error_reported = false # Flag to track if we've already reported an error end |
Instance Attribute Details
#initialized ⇒ Object (readonly)
Returns the value of attribute initialized.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def initialized @initialized end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def name @name end |
#protocol_version ⇒ Object (readonly)
Returns the value of attribute protocol_version.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def protocol_version @protocol_version end |
#responses ⇒ Object (readonly)
Returns the value of attribute responses.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def responses @responses end |
#server_capabilities ⇒ Object (readonly)
Returns the value of attribute server_capabilities.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def server_capabilities @server_capabilities end |
#server_info ⇒ Object (readonly)
Returns the value of attribute server_info.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def server_info @server_info end |
#tool_responses ⇒ Object (readonly)
Returns the value of attribute tool_responses.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def tool_responses @tool_responses end |
#version ⇒ Object (readonly)
Returns the value of attribute version.
18 19 20 |
# File 'lib/encom/client.rb', line 18 def version @version end |
Instance Method Details
#call_tool(name:, arguments:, timeout: 5) ⇒ Hash
Call a tool on the server
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/encom/client.rb', line 246 def call_tool(name:, arguments:, timeout: 5) id = request( { method: 'tools/call', params: { name: name, arguments: arguments } } ) # Wait for the response response = wait_for_response(id, timeout) if response[:error] error_msg = "Tool error: #{response[:error][:message]} (#{response[:error][:code]})" raise ToolError, error_msg end # Return the result content response[:result] end |
#close ⇒ Object
296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/encom/client.rb', line 296 def close return if @closing @closing = true puts 'Closing' return unless @transport @transport.close @transport = nil end |
#connect(transport) ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/encom/client.rb', line 43 def connect(transport) @transport = transport @transport .on_close { close } .on_data { |data| handle_response(data) } .on_error { |error| handle_transport_error(error) } .start # Send initialize request request( { method: 'initialize', params: { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: @capabilities, clientInfo: { name: @name, version: @version } } } ) rescue JSON::ParserError => e # This might be a protocol version error or other startup error trigger_error(ConnectionError.new("Error parsing initial response: #{e.}")) end |
#get_tool_result(request_id) ⇒ Hash?
Get the result of a tool call
284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/encom/client.rb', line 284 def get_tool_result(request_id) response = @tool_responses[request_id] return nil unless response if response[:error] error_msg = "Tool error: #{response[:error][:message]} (#{response[:error][:code]})" raise ToolError, error_msg end response[:result] end |
#get_tools(request_id) ⇒ Array<Hash>?
Get the list of tools from a previous list_tools request
273 274 275 276 277 278 |
# File 'lib/encom/client.rb', line 273 def get_tools(request_id) response = @tool_responses[request_id] return nil unless response && response[:result] response[:result][:tools] end |
#handle_error(response) ⇒ Object
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/encom/client.rb', line 135 def handle_error(response) # Don't process errors if we're already closing return if @closing # Check if this is an error response to an initialize request if @pending_requests[response[:id]] == 'initialize' && response[:error][:code] == Encom::ErrorCodes::PROTOCOL_ERROR error = ProtocolVersionError.new("Unsupported protocol version: #{response[:error][:message]}") close trigger_error(error) return end error_msg = "Error from server: #{response[:error][:message]} (#{response[:error][:code]})" trigger_error(ConnectionError.new(error_msg)) end |
#handle_initialize_result(response) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/encom/client.rb', line 111 def handle_initialize_result(response) @server_info = response[:result][:serverInfo] @server_capabilities = response[:result][:capabilities] @protocol_version = response[:result][:protocolVersion] unless SUPPORTED_PROTOCOL_VERSIONS.include?(@protocol_version) = "Unsupported protocol version: #{@protocol_version}. " \ "This client supports: #{SUPPORTED_PROTOCOL_VERSIONS.join(', ')}" error = ProtocolVersionError.new() close trigger_error(error) return end @initialized = true notification( { method: 'initialized', params: {} } ) end |
#handle_response(data) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/encom/client.rb', line 75 def handle_response(data) data = data.strip if data.is_a?(String) parsed_response = JSON.parse(data, symbolize_names: true) @responses << parsed_response # Check for protocol errors immediately, even without an ID if parsed_response[:error] && parsed_response[:error][:code] == Encom::ErrorCodes::PROTOCOL_ERROR && parsed_response[:error][:message].include?('Unsupported protocol version') # Only trigger a protocol error if we haven't already closed the connection unless @closing error = ProtocolVersionError.new(parsed_response[:error][:message]) close trigger_error(error) end return end if parsed_response[:id] @response_mutex.synchronize do @tool_responses[parsed_response[:id]] = parsed_response @response_condition.broadcast # Signal threads waiting for this response end if parsed_response[:result] handle_initialize_result(parsed_response) if @pending_requests[parsed_response[:id]] == 'initialize' elsif parsed_response[:error] handle_error(parsed_response) end end rescue JSON::ParserError => e error_msg = "Error parsing response: #{e.}, Raw response: #{data.inspect}" trigger_error(ConnectionError.new(error_msg)) end |
#handle_transport_error(error) ⇒ Object
71 72 73 |
# File 'lib/encom/client.rb', line 71 def handle_transport_error(error) trigger_error(ConnectionError.new("Transport error: #{error.}")) end |
#list_tools(params = nil, timeout = 5) ⇒ Array<Hash>
List available tools from the server
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/encom/client.rb', line 216 def list_tools(params = nil, timeout = 5) request_data = { method: 'tools/list' } request_data[:params] = params if params id = request(request_data) # Wait for the response response = wait_for_response(id, timeout) if response[:error] error_msg = "Error from server: #{response[:error][:message]} (#{response[:error][:code]})" raise ConnectionError, error_msg end # Return the tools array response[:result][:tools] end |
#notification(notification_data) ⇒ Object
183 184 185 186 187 188 189 |
# File 'lib/encom/client.rb', line 183 def notification(notification_data) @transport.send( JSON.generate({ jsonrpc: '2.0' }.merge(notification_data)) ) end |
#on_error(&block) ⇒ Object
Register a callback for error handling
38 39 40 41 |
# File 'lib/encom/client.rb', line 38 def on_error(&block) @error_handlers << block self end |
#request(request_data) ⇒ Object
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/encom/client.rb', line 167 def request(request_data) @message_id += 1 id = @message_id @pending_requests[id] = request_data[:method] @transport.send( JSON.generate({ jsonrpc: '2.0', id: id }.merge(request_data)) ) id end |
#trigger_error(error) ⇒ Object
152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/encom/client.rb', line 152 def trigger_error(error) # Only report the first error return if @first_error_reported @first_error_reported = true if @error_handlers.empty? # TODO: I'd love to re-raise this to the user but this ends up run # in a background thread right now due to how we've implement the stdio transport puts "MCP Client Error: #{error.}" else @error_handlers.each { |handler| handler.call(error) } end end |
#wait_for_response(id, timeout = 5) ⇒ Hash
Wait for a response with a specific ID, with timeout
197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/encom/client.rb', line 197 def wait_for_response(id, timeout = 5) deadline = Time.now + timeout @response_mutex.synchronize do @response_condition.wait(@response_mutex, 0.1) while !@tool_responses.key?(id) && Time.now < deadline raise RequestTimeoutError, "Timeout waiting for response to request #{id}" unless @tool_responses.key?(id) @tool_responses[id] end end |